折叠一个有区别的联合——从联合中派生出一个包含所有可能的键值组合的伞型
Collapsing a discriminated union - derive an umbrella type with all possible key-value combinations from the union
我有一个受歧视的工会,例如:
type Union = { a: "foo", b: string, c: number } | {a: "bar", b: boolean }
我需要派生一个包含所有潜在属性的类型,并分配可能在 Union
的任何成员上找到的类型,即使只在某些成员上定义 - 在我的示例中:
type CollapsedUnion = {
a: "foo" | "bar",
b: string | boolean,
c: number | undefined
}
如何创建派生此类折叠联合的泛型?
我需要一个支持任意大小联合的泛型。
可以实现类似的行为 as a byproduct by using native Omit
utility,但对我来说不幸的是,它遗漏了每个联合成员都不存在的属性(不将它们计入 undefined
或通过 ?
).
可行;下面介绍了一种可能的解决方案。如果有可能以更简单的方式实现它,我会很感兴趣。我添加了注释来引导您完成代码。
// an axiliary type -- we need to postpone creating a proper union, as a tuple type can be traversed recursively
// I added additional branch to make the task a bit harder / to make sure it works in a more generic case
type ProtoUnion = [{ a: "foo", b: string, c: number }, {a: "bar", b: boolean }, { c: string }]
// an axiliary type to recover proper Union
type CollapseToUnion<T extends Record<string, any>[], Acc = {}> = // starting with a tuple of records and accumulator
T extends [infer H, ...infer Rest] ? // traverse
Rest extends Record<string, any>[] ? // if still a record present
CollapseToUnion<Rest, (H | Acc)> : // recursive call: collapse as union
// in other cases return accumulator
Acc :
Acc
// union recovered
type Union = CollapseToUnion<ProtoUnion>
// this type is empty, so starting with union is _impossible_ to recover all needed keys in a generic way
type UnionKeys = keyof Union
// this type achieves what you are asking for but only for 2 types
type MergeAsValuesUnion<A, B> = { [K in (keyof A | keyof B)]:
K extends keyof A ?
K extends keyof B ? A[K] | B[K] :
A[K] | undefined :
K extends keyof B ? B[K] | undefined :
never
}
type OriginalUnionIntersected = MergeAsValuesUnion<ProtoUnion[0], ProtoUnion[1]>
/*
type OriginalUnionIntersected = {
a: "foo" | "bar";
b: string | boolean;
c: number | undefined;
}
*/
// this is works exactly the same as CollapseToUnion, but instead of reducing with |
// it uses MergeAsValuesUnion to reduce
type CollapseToIntersetion<T extends Record<string, any>[], Acc = {}> = T extends [infer H, ...infer Rest] ?
Rest extends Record<string, any>[] ?
CollapseToIntersetion<Rest, MergeAsValuesUnion<H, Acc>>
: Acc : Acc
const i: CollapseToIntersetion<ProtoUnion> = {
a: 'bar', // "bar" | "foo" | undefined
b: true, // string | boolean | undefined
c: undefined // string | number | undefined
}
编辑:
CollapseToIntersetion
被咬掉了。以 {}
作为默认累加器开始导致值类型具有 | undefined
。
// this is works exactly the same as CollapseToUnion,
// but instead of reducing with | -- it uses MergeAsValuesUnion to reduce;
// Acc = T[0] since Acc = {} would result in all values types unioned with undefined
type CollapseToIntersetion<T extends Record<string, any>[], Acc = T[0]> = T extends [infer H, ...infer Rest] ?
Rest extends Record<string, any>[] ?
CollapseToIntersetion<Rest, MergeAsValuesUnion<H, Acc>>
: Acc : Acc
我找到了 一个 两个 条路!
编辑:这是一个具有两个独立类型参数的解决方案。请参阅下方以了解具有单个联合类型参数的解决方案。
// The source types
type A = { a: "foo", b: string, c: number }
type B = { a: "bar", b: boolean }
// This utility lets T be indexed by any (string) key
type Indexify<T> = T & { [str: string]: undefined; }
// Where the magic happens ✨
type AllFields<T, R> = { [K in keyof (T & R) & string]: Indexify<T | R>[K] }
type Result = AllFields<A, B>
/**
*
* type Result = {
* a: "foo" | "bar";
* b: string | boolean;
* c: number | undefined;
* }
*/
工作原理
AllFields
是映射类型。映射类型的 'key' 部分
[K in keyof (T & R) & string]
表示 K
扩展联合 T & R
的键,这意味着它将是 T
或 [=19= 中所有键的联合].这是第一步。它确保我们正在创建一个包含所有必需键的对象。
& string
是必需的,因为它指定 K
也必须是字符串。无论如何,情况几乎总是如此,因为 JS 中的所有对象键都是字符串(偶数)——符号除外,但无论如何它们都是不同的鱼。
类型表达式
Indexify<T | R>
returns T
和 R
的联合类型,但添加了字符串索引。这意味着如果我们尝试通过 [ 索引它,TS 不会抛出错误=16=] 即使 K
在 T
或 R
.
之一中不存在
最后
Indexify<T | R>[K]
意味着我们正在通过 K
索引这个 union-with-undefineds-for-string-indexes。其中,如果 K
是 T
、R
或两者的键,将导致该键的值类型。
否则,它将返回到 [string]: undefined
索引并导致值为未定义。
编辑:单个通用参数的解决方案
您指定您实际上不希望它对两个类型参数起作用,而是对现有的联合类型起作用,而不管该联合中有多少成员。
付出了血汗和泪水,但我做到了。
// Magic as far as I'm concerned.
// Taken from
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
// This utility lets T be indexed by any key
type Indexify<T> = T & { [str: string]: undefined; }
// To make a type where all values are undefined, so that in AllUnionKeys<T>
// TS doesn't remove the keys whose values are incompatible, e.g. string & number
type UndefinedVals<T> = { [K in keyof T]: undefined }
// This returns a union of all keys present across all members of the union T
type AllUnionKeys<T> = keyof UnionToIntersection<UndefinedVals<T>>
// Where the (rest of the) magic happens ✨
type AllFields<T> = { [K in AllUnionKeys<T> & string]: Indexify<T>[K] }
// The source types
type A = { a: "foo", b: string, c: number }
type B = { a: "bar", b: boolean; }
type Union = A | B
type Result = AllFields<Union>
/**
*
* type Result = {
* a: "foo" | "bar";
* b: string | boolean;
* c: number | undefined;
* }
*/
我从@jcalz 的精彩回答中得到 。我试图理解它,但做不到。不管怎样,我们可以把它当作一个将联合类型转换为交集类型的魔盒。这就是我们需要得到我们想要的结果的全部。
这个基于 Aron 的回答的解决方案递归地深度折叠联合,而不仅仅是在顶层:
export type ExtractObjects<T> = Extract<T, Record<keyof any, any>>
export type ExcludeObjects<T> = Exclude<T, Record<keyof any, any>>
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never
export type Indexify<T> = T & { [str: string]: undefined }
export type AllUnionKeys<T> = keyof UnionToIntersection<{ [K in keyof T]: undefined }>
//
export type CollapseUnionOfOnlyObjects<T extends Record<keyof any, any>> = {
[K in AllUnionKeys<T> & string]: Indexify<T>[K]
}
type ExtractAndCollapseObjects<T> = CollapseUnionOfOnlyObjects<ExtractObjects<T>>
// recursive union collapse
export type CollapseUnion<T> = ExtractObjects<T> extends never
? T
:
| {
[K in keyof ExtractAndCollapseObjects<T>]: CollapseUnion<ExtractAndCollapseObjects<T>[K]>
}
| ExcludeObjects<T>
我有一个受歧视的工会,例如:
type Union = { a: "foo", b: string, c: number } | {a: "bar", b: boolean }
我需要派生一个包含所有潜在属性的类型,并分配可能在 Union
的任何成员上找到的类型,即使只在某些成员上定义 - 在我的示例中:
type CollapsedUnion = {
a: "foo" | "bar",
b: string | boolean,
c: number | undefined
}
如何创建派生此类折叠联合的泛型?
我需要一个支持任意大小联合的泛型。
可以实现类似的行为 as a byproduct by using native Omit
utility,但对我来说不幸的是,它遗漏了每个联合成员都不存在的属性(不将它们计入 undefined
或通过 ?
).
可行;下面介绍了一种可能的解决方案。如果有可能以更简单的方式实现它,我会很感兴趣。我添加了注释来引导您完成代码。
// an axiliary type -- we need to postpone creating a proper union, as a tuple type can be traversed recursively
// I added additional branch to make the task a bit harder / to make sure it works in a more generic case
type ProtoUnion = [{ a: "foo", b: string, c: number }, {a: "bar", b: boolean }, { c: string }]
// an axiliary type to recover proper Union
type CollapseToUnion<T extends Record<string, any>[], Acc = {}> = // starting with a tuple of records and accumulator
T extends [infer H, ...infer Rest] ? // traverse
Rest extends Record<string, any>[] ? // if still a record present
CollapseToUnion<Rest, (H | Acc)> : // recursive call: collapse as union
// in other cases return accumulator
Acc :
Acc
// union recovered
type Union = CollapseToUnion<ProtoUnion>
// this type is empty, so starting with union is _impossible_ to recover all needed keys in a generic way
type UnionKeys = keyof Union
// this type achieves what you are asking for but only for 2 types
type MergeAsValuesUnion<A, B> = { [K in (keyof A | keyof B)]:
K extends keyof A ?
K extends keyof B ? A[K] | B[K] :
A[K] | undefined :
K extends keyof B ? B[K] | undefined :
never
}
type OriginalUnionIntersected = MergeAsValuesUnion<ProtoUnion[0], ProtoUnion[1]>
/*
type OriginalUnionIntersected = {
a: "foo" | "bar";
b: string | boolean;
c: number | undefined;
}
*/
// this is works exactly the same as CollapseToUnion, but instead of reducing with |
// it uses MergeAsValuesUnion to reduce
type CollapseToIntersetion<T extends Record<string, any>[], Acc = {}> = T extends [infer H, ...infer Rest] ?
Rest extends Record<string, any>[] ?
CollapseToIntersetion<Rest, MergeAsValuesUnion<H, Acc>>
: Acc : Acc
const i: CollapseToIntersetion<ProtoUnion> = {
a: 'bar', // "bar" | "foo" | undefined
b: true, // string | boolean | undefined
c: undefined // string | number | undefined
}
编辑:
CollapseToIntersetion
被咬掉了。以 {}
作为默认累加器开始导致值类型具有 | undefined
。
// this is works exactly the same as CollapseToUnion,
// but instead of reducing with | -- it uses MergeAsValuesUnion to reduce;
// Acc = T[0] since Acc = {} would result in all values types unioned with undefined
type CollapseToIntersetion<T extends Record<string, any>[], Acc = T[0]> = T extends [infer H, ...infer Rest] ?
Rest extends Record<string, any>[] ?
CollapseToIntersetion<Rest, MergeAsValuesUnion<H, Acc>>
: Acc : Acc
我找到了 一个 两个 条路!
编辑:这是一个具有两个独立类型参数的解决方案。请参阅下方以了解具有单个联合类型参数的解决方案。
// The source types
type A = { a: "foo", b: string, c: number }
type B = { a: "bar", b: boolean }
// This utility lets T be indexed by any (string) key
type Indexify<T> = T & { [str: string]: undefined; }
// Where the magic happens ✨
type AllFields<T, R> = { [K in keyof (T & R) & string]: Indexify<T | R>[K] }
type Result = AllFields<A, B>
/**
*
* type Result = {
* a: "foo" | "bar";
* b: string | boolean;
* c: number | undefined;
* }
*/
工作原理
AllFields
是映射类型。映射类型的 'key' 部分
[K in keyof (T & R) & string]
表示 K
扩展联合 T & R
的键,这意味着它将是 T
或 [=19= 中所有键的联合].这是第一步。它确保我们正在创建一个包含所有必需键的对象。
& string
是必需的,因为它指定 K
也必须是字符串。无论如何,情况几乎总是如此,因为 JS 中的所有对象键都是字符串(偶数)——符号除外,但无论如何它们都是不同的鱼。
类型表达式
Indexify<T | R>
returns T
和 R
的联合类型,但添加了字符串索引。这意味着如果我们尝试通过 [ 索引它,TS 不会抛出错误=16=] 即使 K
在 T
或 R
.
最后
Indexify<T | R>[K]
意味着我们正在通过 K
索引这个 union-with-undefineds-for-string-indexes。其中,如果 K
是 T
、R
或两者的键,将导致该键的值类型。
否则,它将返回到 [string]: undefined
索引并导致值为未定义。
编辑:单个通用参数的解决方案
您指定您实际上不希望它对两个类型参数起作用,而是对现有的联合类型起作用,而不管该联合中有多少成员。
付出了血汗和泪水,但我做到了。
// Magic as far as I'm concerned.
// Taken from
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
// This utility lets T be indexed by any key
type Indexify<T> = T & { [str: string]: undefined; }
// To make a type where all values are undefined, so that in AllUnionKeys<T>
// TS doesn't remove the keys whose values are incompatible, e.g. string & number
type UndefinedVals<T> = { [K in keyof T]: undefined }
// This returns a union of all keys present across all members of the union T
type AllUnionKeys<T> = keyof UnionToIntersection<UndefinedVals<T>>
// Where the (rest of the) magic happens ✨
type AllFields<T> = { [K in AllUnionKeys<T> & string]: Indexify<T>[K] }
// The source types
type A = { a: "foo", b: string, c: number }
type B = { a: "bar", b: boolean; }
type Union = A | B
type Result = AllFields<Union>
/**
*
* type Result = {
* a: "foo" | "bar";
* b: string | boolean;
* c: number | undefined;
* }
*/
我从@jcalz 的精彩回答中得到
这个基于 Aron 的回答的解决方案递归地深度折叠联合,而不仅仅是在顶层:
export type ExtractObjects<T> = Extract<T, Record<keyof any, any>>
export type ExcludeObjects<T> = Exclude<T, Record<keyof any, any>>
export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I
) => void
? I
: never
export type Indexify<T> = T & { [str: string]: undefined }
export type AllUnionKeys<T> = keyof UnionToIntersection<{ [K in keyof T]: undefined }>
//
export type CollapseUnionOfOnlyObjects<T extends Record<keyof any, any>> = {
[K in AllUnionKeys<T> & string]: Indexify<T>[K]
}
type ExtractAndCollapseObjects<T> = CollapseUnionOfOnlyObjects<ExtractObjects<T>>
// recursive union collapse
export type CollapseUnion<T> = ExtractObjects<T> extends never
? T
:
| {
[K in keyof ExtractAndCollapseObjects<T>]: CollapseUnion<ExtractAndCollapseObjects<T>[K]>
}
| ExcludeObjects<T>