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