打字稿:将类型定义为类型与部分存在的对象的联合
Typescript: Define type as union of types with a partially present object
假设我有两个这样定义的类型:
const type A = { identifier: string, properties: { p1: string, p2: string }};
const type B = { identifier: string, properties: { p3: string, p4: string }};
我想将类型定义为 A 和 B 的联合,其中 'properties' 键将被部分填充(但始终存在,即至少应存在一个键)。但是,我不想混合使用这两种类型的 'properties' 对象的键。
例如,这些实例是有效的:
{ identifier: 'id', properties: { p1: 'prop1', p2: 'prop2' }}
{ identifier: 'id', properties: { p2: 'prop2' }}
{ identifier: 'id', properties: { p3: 'prop3', p4: 'prop4' }}
{ identifier: 'id', properties: { p3: 'prop3' }}
但这些实例无效:
{ identifier: 'id', properties: {}} // 'properties' is empty
{ identifier: 'id' } // 'properties' is missing
{ identifier: 'id', properties: { p1: 'prop1', p3: 'prop3' }} // 'properties' has keys from A and B
我尝试了很多不同的配置,但似乎没有任何效果。另外,我没有找到任何问题来回答这个问题。
这看起来像是以下两个问题的组合:
"" make a type function AtLeastOneProp<T>
which takes turns an object type T
into a Partial<T>
-like 类型但它不允许所有属性都丢失:
type AtLeastOneProp<T extends object> = T extends object ? { [K in keyof T]-?:
Pick<T, K> & Partial<T> extends infer O ? { [P in keyof O]: O[P] } : never }[keyof T]
: never;
这里我使用 distributive conditional types, conditional type inference, and various utility and mapped types 遍历 T
的每个 属性 K
并生成 T
的版本,其中只有 属性 是必需的 (Pick<T, K>
),其余的是可选的 (Partial<T>
),并生成它们的并集。
如果我们这样做,例如 A
的 properties
属性,我们得到:
type AtLeastOnePropFromAProperties = AtLeastOneProp<A["properties"]>;
/* type AtLeastOnePropFromAProperties = {
p1: string;
p2?: string | undefined;
} | {
p2: string;
p1?: string | undefined;
} */
您可以看到要么需要 p1
而 p2
不需要,要么需要 p2
而 p1
不需要。
"" make a type function ExclusifyUnion<T>
which turns a union 对象类型 T
转换为此类类型的 独占 联合,其中每个联合成员拒绝属于其他成员的属性:
type AllKeys<T> = T extends any ? keyof T : never;
type ExclusifyUnion<T, K extends AllKeys<T> = AllKeys<T>> =
T extends any ? (T & Partial<Record<Exclude<K, keyof T>, never>>) extends
infer O ? { [P in keyof O]: O[P] } : never : never;
这也是使用分布式条件类型和实用类型。 AllKeys<T>
是 T
的所有联合成员的所有键的联合,ExclusifyUnion<T>
获取 T
的每个联合成员并添加 Partial<Record<Exclude<K, keyof T>, never>>
,其中 K
是来自完整联合的 AllKeys<T>
。 Exclude<K, keyof T>
仅查看当前联合成员中 不 的完整联合中的那些键,并且 Partial<Record<..., never>>
生成一个对象类型,其在这些键上的值必须失踪(或undefined
)。
如果我们在 A
和 B
的 properties
的并集上使用它,我们得到:
type ExclusifyAorBProperties = ExclusifyUnion<A["properties"] | B["properties"]>
/* type ExclusifyAorBProperties = {
p1: string;
p2: string;
p3?: undefined;
p4?: undefined;
} | {
p3: string;
p4: string;
p1?: undefined;
p2?: undefined;
} */
你可以看到p1
和p2
是必需的,而p3
和p4
是禁止的,或者p3
和p4
是必需的,而 p1
和 p2
是禁止的。
对于您想要的类型,我们会将 AtLeastOneProp<T>
和 ExclusifyUnion<T>
与您的 A
和 B
类型组合:
type MyType = {
identifier: string,
properties: ExclusifyUnion<AtLeastOneProp<A["properties"] | B["properties"]>>
}
如果您使用 IntelliSense 检查 properties
,您将看到它是如何工作的,至少在类型被截断之前是这样:
/* (property) properties: {
p1: string;
p2?: string | undefined;
p3?: undefined;
p4?: undefined;
} | {
p2: string;
p1?: string | undefined;
p3?: undefined;
p4?: undefined;
} | {
p3: string;
p4?: string | undefined;
p1?: undefined;
p2?: undefined;
} | {
...;
} */
让我们在您的用例中对其进行测试:
const a: MyType = { identifier: 'id', properties: { p1: 'prop1', p2: 'prop2' } }; // ok
const b: MyType = { identifier: 'id', properties: { p2: 'prop2' } }; // ok
const c: MyType = { identifier: 'id', properties: { p3: 'prop3', p4: 'prop4' } }; // ok
const d: MyType = { identifier: 'id', properties: { p3: 'prop3' } }; // ok
const e: MyType = { identifier: 'id', properties: {} } // error!
// Type '{}' is not assignable -----> ~~~~~~~~~~
const f: MyType = { identifier: 'id' } // error!
// ~ <-- Property 'properties' is missing
const g: MyType = { identifier: 'id', properties: { p1: 'prop1', p3: 'prop3' } } // error!
// ---------------------------------> ~~~~~~~~~~
// Type '{ p1: string; p3: string; } is not assignable
看起来不错!
假设我有两个这样定义的类型:
const type A = { identifier: string, properties: { p1: string, p2: string }};
const type B = { identifier: string, properties: { p3: string, p4: string }};
我想将类型定义为 A 和 B 的联合,其中 'properties' 键将被部分填充(但始终存在,即至少应存在一个键)。但是,我不想混合使用这两种类型的 'properties' 对象的键。
例如,这些实例是有效的:
{ identifier: 'id', properties: { p1: 'prop1', p2: 'prop2' }}
{ identifier: 'id', properties: { p2: 'prop2' }}
{ identifier: 'id', properties: { p3: 'prop3', p4: 'prop4' }}
{ identifier: 'id', properties: { p3: 'prop3' }}
但这些实例无效:
{ identifier: 'id', properties: {}} // 'properties' is empty
{ identifier: 'id' } // 'properties' is missing
{ identifier: 'id', properties: { p1: 'prop1', p3: 'prop3' }} // 'properties' has keys from A and B
我尝试了很多不同的配置,但似乎没有任何效果。另外,我没有找到任何问题来回答这个问题。
这看起来像是以下两个问题的组合:
"AtLeastOneProp<T>
which takes turns an object type T
into a Partial<T>
-like 类型但它不允许所有属性都丢失:
type AtLeastOneProp<T extends object> = T extends object ? { [K in keyof T]-?:
Pick<T, K> & Partial<T> extends infer O ? { [P in keyof O]: O[P] } : never }[keyof T]
: never;
这里我使用 distributive conditional types, conditional type inference, and various utility and mapped types 遍历 T
的每个 属性 K
并生成 T
的版本,其中只有 属性 是必需的 (Pick<T, K>
),其余的是可选的 (Partial<T>
),并生成它们的并集。
如果我们这样做,例如 A
的 properties
属性,我们得到:
type AtLeastOnePropFromAProperties = AtLeastOneProp<A["properties"]>;
/* type AtLeastOnePropFromAProperties = {
p1: string;
p2?: string | undefined;
} | {
p2: string;
p1?: string | undefined;
} */
您可以看到要么需要 p1
而 p2
不需要,要么需要 p2
而 p1
不需要。
"ExclusifyUnion<T>
which turns a union 对象类型 T
转换为此类类型的 独占 联合,其中每个联合成员拒绝属于其他成员的属性:
type AllKeys<T> = T extends any ? keyof T : never;
type ExclusifyUnion<T, K extends AllKeys<T> = AllKeys<T>> =
T extends any ? (T & Partial<Record<Exclude<K, keyof T>, never>>) extends
infer O ? { [P in keyof O]: O[P] } : never : never;
这也是使用分布式条件类型和实用类型。 AllKeys<T>
是 T
的所有联合成员的所有键的联合,ExclusifyUnion<T>
获取 T
的每个联合成员并添加 Partial<Record<Exclude<K, keyof T>, never>>
,其中 K
是来自完整联合的 AllKeys<T>
。 Exclude<K, keyof T>
仅查看当前联合成员中 不 的完整联合中的那些键,并且 Partial<Record<..., never>>
生成一个对象类型,其在这些键上的值必须失踪(或undefined
)。
如果我们在 A
和 B
的 properties
的并集上使用它,我们得到:
type ExclusifyAorBProperties = ExclusifyUnion<A["properties"] | B["properties"]>
/* type ExclusifyAorBProperties = {
p1: string;
p2: string;
p3?: undefined;
p4?: undefined;
} | {
p3: string;
p4: string;
p1?: undefined;
p2?: undefined;
} */
你可以看到p1
和p2
是必需的,而p3
和p4
是禁止的,或者p3
和p4
是必需的,而 p1
和 p2
是禁止的。
对于您想要的类型,我们会将 AtLeastOneProp<T>
和 ExclusifyUnion<T>
与您的 A
和 B
类型组合:
type MyType = {
identifier: string,
properties: ExclusifyUnion<AtLeastOneProp<A["properties"] | B["properties"]>>
}
如果您使用 IntelliSense 检查 properties
,您将看到它是如何工作的,至少在类型被截断之前是这样:
/* (property) properties: {
p1: string;
p2?: string | undefined;
p3?: undefined;
p4?: undefined;
} | {
p2: string;
p1?: string | undefined;
p3?: undefined;
p4?: undefined;
} | {
p3: string;
p4?: string | undefined;
p1?: undefined;
p2?: undefined;
} | {
...;
} */
让我们在您的用例中对其进行测试:
const a: MyType = { identifier: 'id', properties: { p1: 'prop1', p2: 'prop2' } }; // ok
const b: MyType = { identifier: 'id', properties: { p2: 'prop2' } }; // ok
const c: MyType = { identifier: 'id', properties: { p3: 'prop3', p4: 'prop4' } }; // ok
const d: MyType = { identifier: 'id', properties: { p3: 'prop3' } }; // ok
const e: MyType = { identifier: 'id', properties: {} } // error!
// Type '{}' is not assignable -----> ~~~~~~~~~~
const f: MyType = { identifier: 'id' } // error!
// ~ <-- Property 'properties' is missing
const g: MyType = { identifier: 'id', properties: { p1: 'prop1', p3: 'prop3' } } // error!
// ---------------------------------> ~~~~~~~~~~
// Type '{ p1: string; p3: string; } is not assignable
看起来不错!