折叠可区分的联合 - 从联合派生出具有所有可能的键值组合的伞类型

问题描述 投票:0回答:4

我有一个受歧视的工会,例如:

type Union = { a: "foo", b: string, c: number } | {a: "bar", b: boolean }

我需要派生一个包含所有潜在属性的类型,并分配可以在

Union
的任何成员上找到的类型,即使仅在某些成员上定义 - 在我的示例中:

type CollapsedUnion = { 
  a: "foo" | "bar", 
  b: string | boolean, 
  c: number | undefined 
}

我怎样才能创建一个派生出这种折叠联合的泛型?
我需要一个支持任何规模联合体的泛型。

通过使用本机Omit

实用程序
可以实现类似的行为作为副产品,但不幸的是,对我来说,它遗漏了每个工会成员中不存在的属性(不将它们计为
undefined
或通过
?
).

typescript generics union discriminated-union
4个回答
10
投票

我找到了一个两种方式!

编辑:这是一个具有两个单独类型参数的解决方案。请参阅下方的具有单个联合类型参数的解决方案。

// 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
是映射类型。映射类型的“关键”部分

[K in keyof (T & R) & string]

意味着

K
扩展了并集
T & R
的键,这意味着它将成为
T
R
中所有键的并集。这是第一步。它确保我们使用所有必需的键来创建一个对象。

& string
是必需的,因为它指定
K
也必须是字符串。无论如何,情况几乎总是如此,因为 JS 中的所有对象键都是字符串(偶数)——除了符号,但无论如何,这些都是不同的鱼。

类型表达式

Indexify<T | R>

返回

T
R
的并集类型,但添加了字符串索引。这意味着,如果我们尝试通过
K
对其进行索引,即使
K
不存在,TS 也不会抛出错误
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 https://stackoverflow.com/a/50375286/3229534
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 的精彩回答中得到了

UnionToIntersection
。我试图理解它,但无法理解。无论如何,我们可以将其视为一个将并集类型转换为交集类型的魔术盒。这就是我们获得想要的结果所需要的。

新 TS 游乐场链接


1
投票

这是可行的;下面介绍了一种可能的解决方案。如果可以以更简单的方式实现它,我会很感兴趣。我添加了注释来引导您完成代码。

// 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

游乐场


0
投票

这个解决方案基于阿伦的答案,递归地深度折叠联合,而不仅仅是在顶层:

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 }>
// https://stackoverflow.com/questions/65750673/collapsing-a-discriminated-union-derive-an-umbrella-type-with-all-possible-key
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>

0
投票

将我的版本留在这里,因为经过几次实验后发现它可以完成很多更简单:

type MergedUnion<T extends object> = {
    [key in keyof T & string]: T[key];
};

不需要嵌套类型实用程序的多级层次结构。

type A = { type: "A", data: number; }
type B = { type: "B", data: string; }

type AorB = A | B; 
//    ^-- { type: "A", data: number; } | { type: "B", data: string; } 

type AB = MergedUnion<A | B>; 
//    ^-- { type: "A" | "B", data: number | string; }

游乐场链接

© www.soinside.com 2019 - 2024. All rights reserved.