[英]TypeScript deep intersection of objects union
問題
我想實現執行以下操作的DeepIntersection
類型:
type Input =
| { message: string | number; type: "a"; a: number }
| { message: string | boolean; type: "b"; b: number };
type DeepIntersection<T> = /* TO IMPLEMENT */ T;
type Output = DeepIntersection<Input>;
// => { message: string; type: never; a: never; b: never };
// which then can be easly converted to => { message: string }
我知道如何制作這些:
// With Pick<T, keyof T>
type AlmostThereOutput1 = { message: string | number | boolean; type: "a" | "b" };
// With https://stackoverflow.com/a/47375979/11545965 but '&' instead of '|'
type AlmostThereOutput2 =
| { message: string | number; type: "a" }
| { message: string | boolean; type: "b" };
但不幸的是,我想不出如何生產我真正想要的東西。
語境
假設我們有一個不同類型的對象數組(可能是一個區分聯合),我們想要實現一個可以通過它更新任何項目的 function。 這個 function 可以接受:
(prevItem: Item) => Item
(react useState style)commonProps: Partial<DeepIntersection<Item>>
可以安全地注入到任何現有項目中,例如{...prevItem, ...commonProps}
。編輯 - 附加說明:
我稱它為DeepIntersection
是因為交集運算符&
對於非對象類型的工作方式如下:
type Intersection = 'a' & ('b' | 'a');
// => 'a'
這正是我需要的行為,但更深入地應用於每個屬性。
所以在我的例子中,它將是:
type Message = (string | number) & (string | boolean); // string
type Type = "a" & "b"; // never
type A = number & undefined; // never
type B = undefined & number; // never
never
s 的結果也很好:
type Result = { message: string; type: never; a: never; b: never };
因為刪除never
屬性非常容易。
目前我沒有考慮遞歸解決方案,但如果可以實現,我很樂意看到兩者。 我考慮使用這個助手來解決這個問題: TypeScript allowed unsafe Partial usage in generic
這是一種可能的方法:
type Combine<T, K extends keyof T = keyof T> = { [P in K]:
(T extends unknown ? (x: T[P]) => void : never) extends
((x: infer I) => void) ? I : never
} extends infer O ?
{ [K in keyof O as O[K] extends never ? never : K]: O[K] } : never
這是它的工作原理。 首先,我需要將T
本身視為單個聯合類型的事物,以便獲得聯合的所有成員共享的密鑰。 keyof
類型運算符的操作數是逆變的(有關方差的描述,請參閱此 q/a ),因此如果T
是聯合,則keyof T
是鍵的交集。 (例如, keyof (A | B | C)
等價於(keyof A) & (keyof B) & (keyof C)
。)我將其預先計算為K
,因此我們可以創建一個映射類型僅迭代那些共享鍵,如{[P in K]: ...}
。 對於Input
,這只是message
和type
。
其次,我還需要將T
視為我們分配類型操作的聯合。 我希望能夠將T
拆分為其工會成員並與這些成員一起做事。 這就是為什么我有(T extends unknown? ... : never)
。 事實上整個部分(T extends unknown? (x: T[P]) => void: never) extends ((x: infer I) => void)? I: never
(T extends unknown? (x: T[P]) => void: never) extends ((x: infer I) => void)? I: never
使用此 q/a中的UnionToIntersection<T>
技術將T
的每個共享鍵處的屬性並集轉換為交集。 因此,對於message
,這將是(string | number) & (string | boolean)
,對於type
,這將是"a" & "b"
。
最后,我們要消除任何交點減少為never
的屬性。 這就是extends infer O? { [K in keyof O as O[K] extends never? never: K]: O[K] }: never
extends infer O? { [K in keyof O as O[K] extends never? never: K]: O[K] }: never
extends infer O? { [K in keyof O as O[K] extends never? never: K]: O[K] }: never
。 它采用映射類型,將其復制到新的類型參數O
,然后使用鍵重新映射過濾掉值類型為never
的任何屬性。
讓我們在Input
上測試一下:
type Input =
| { message: string | number; type: "a"; a: number }
| { message: string | boolean; type: "b"; b: number };
type Output = Combine<Input>;
/* type Output = {
message: string;
} */
看起來不錯!
請注意,這只適用於一層深度(例如,它不會{a: {b: 0 | 1}} | {a: {b: 1 | 2}}
進入{a: {b: 1}}
) ,並且可能還有很多其他邊緣情況,上面的Combine<T>
實現做了一些與你想要的不同的事情。 希望您可以以此為起點。 但是,當然,這種類型的操作是非常棘手的; 目前尚不清楚編譯器如何以及何時決定將聯合類型視為單一內聚類型,將其拆分為多個片段並將 forms 的結果返回為一個聯合,以及將其拆分為多個片段並將 forms 的結果返回為交集. 看似微小的改變將從一種行為轉變為另一種行為。 這些行為(大部分)都有記錄,但很容易出錯。 所以請注意!
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.