簡體   English   中英

折疊一個有區別的聯合 - 從聯合中派生一個包含所有可能鍵值組合的傘類型

[英]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 
}

我怎樣才能制作一個派生這種折疊聯合的泛型?
我需要一個支持任何規模聯合的泛型。

類似的行為可以通過使用本機Omit實用程序作為副產品來實現,但不幸的是,它忽略了每個聯合成員上不存在的屬性(不將它們計入undefined或通過? )。

我找到兩種方法!

編輯:這是一個具有兩個單獨類型參數的解決方案。 請參閱下方以獲取具有單個聯合類型參數的解決方案。

// 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的鍵,這意味着它將是TR中的所有鍵的聯合。 這是第一步。 它確保我們正在制作具有所有必需密鑰的 object。

& string是必需的,因為它指定K也必須是字符串。 無論如何,情況幾乎總是如此,因為 JS 中的所有 object 鍵都是字符串(偶數)——除了符號,但無論如何這些都是不同的魚。

類型表達式

Indexify<T | R>

返回TR的聯合類型,但添加了字符串索引。這意味着即使在TR之一中不存在K時,如果我們嘗試按K對其進行索引,TS 也不會引發錯誤。

最后

Indexify<T | R>[K]

意味着我們正在用K索引這個 union-with-undefineds-for-string-indexes。 其中,如果KTR或兩者的鍵,將導致該鍵的值類型。

否則,它將回退到[string]: undefined索引並導致值為 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;
 * }
 */

我從UnionToIntersection的精彩回答中得到了 UnionToIntersection。 我試圖理解它,但不能。 無論如何,我們可以將其視為一個將聯合類型轉換為交集類型的魔術盒。 這就是我們獲得我們想要的結果所需要的一切。

新的 TS 游樂場鏈接

這是可行的; 下面介紹了一種可能的解決方案。 如果有可能以更簡單的方式實現它,我會很感興趣。 我添加了注釋來引導您完成代碼。

// 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 在值類型中| 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

操場

這個基於 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 }>
// 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>

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM