简体   繁体   English

TypeScript 深交集对象联合

[英]TypeScript deep intersection of objects union

Issue问题

I would like to implement DeepIntersection type that does following:我想实现执行以下操作的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 }

I know how to produce these:我知道如何制作这些:

// 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" };

but unfortunatly I ran out of ideas how to produce what I actually want.但不幸的是,我想不出如何生产我真正想要的东西。

Context语境

Let's say that we have an array of objects of various types (possibly a discrimination union), and we want to implement a function that can update any item by it.假设我们有一个不同类型的对象数组(可能是一个区分联合),我们想要实现一个可以通过它更新任何项目的 function。 This function could accept:这个 function 可以接受:

  • callback (prevItem: Item) => Item (react useState style) callback (prevItem: Item) => Item (react useState style)
  • data commonProps: Partial<DeepIntersection<Item>> that can be safely injected into any existing item as such {...prevItem, ...commonProps} .数据commonProps: Partial<DeepIntersection<Item>>可以安全地注入到任何现有项目中,例如{...prevItem, ...commonProps}

EDIT - Additional explanation:编辑 - 附加说明:

I called it DeepIntersection because intersection operator & works like this for non-object types:我称它为DeepIntersection是因为交集运算符&对于非对象类型的工作方式如下:

type Intersection = 'a' & ('b' | 'a');
// => 'a'

This is exactly the behavior that I need but applied deeper for every property.这正是我需要的行为,但更深入地应用于每个属性。

So in my example it would be:所以在我的例子中,它将是:

type Message = (string | number) & (string | boolean); // string
type Type = "a" & "b"; // never
type A = number & undefined; // never
type B = undefined & number; // never

Result with never s is perfectly fine too: never s 的结果也很好:

type Result = { message: string; type: never; a: never; b: never };

since removing never properties is quite easy.因为删除never属性非常容易。

Currently I didn't think about a recursive solution, but if it's possible to implement I'd love to see both.目前我没有考虑递归解决方案,但如果可以实现,我很乐意看到两者。 I think about using this helper to possibly solve this: TypeScript allows unsafe Partial usage in generic我考虑使用这个助手来解决这个问题: TypeScript allowed unsafe Partial usage in generic

Here is one possible approach:这是一种可能的方法:

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

Here's how it works.这是它的工作原理。 First, I need to treat T as a single union-typed thing by itself, in order to get the keys shared by all members of the union.首先,我需要将T本身视为单个联合类型的事物,以便获得联合的所有成员共享的密钥。 The keyof type operator is contravariant in its operand (see this q/a for a description of variance), so if T is a union, then keyof T is an intersection of the keys. keyof类型运算符的操作数是逆变的(有关方差的描述,请参阅此 q/a ),因此如果T是联合,则keyof T是键的交集。 (eg, keyof (A | B | C) is equivalent to (keyof A) & (keyof B) & (keyof C) .) This I pre-compute as K so we can make a mapped type iterating over just those shared keys, as {[P in K]: ...} . (例如, keyof (A | B | C)等价于(keyof A) & (keyof B) & (keyof C) 。)我将其预先计算为K ,因此我们可以创建一个映射类型仅迭代那些共享键,如{[P in K]: ...} In the case of Input , that's just message and type .对于Input ,这只是messagetype

Second, I also need to treat T as a union over which we distribute a type operation.其次,我需要将T视为我们分配类型操作的联合。 I want to be able to split T into its union members and do stuff with those members.我希望能够将T拆分为其工会成员并与这些成员一起做事。 And that is why I've got (T extends unknown? ... : never) .这就是为什么我有(T extends unknown? ... : never) In fact the whole section (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 (T extends unknown? (x: T[P]) => void: never) extends ((x: infer I) => void)? I: never uses the UnionToIntersection<T> technique as in this q/a to convert the union of properties at each shared key of T into an intersection. (T extends unknown? (x: T[P]) => void: never) extends ((x: infer I) => void)? I: never使用此 q/a中的UnionToIntersection<T>技术将T的每个共享键处的属性并集转换为交集。 So for message this would be (string | number) & (string | boolean) , and for type this would be "a" & "b" .因此,对于message ,这将是(string | number) & (string | boolean) ,对于type ,这将是"a" & "b"

Finally, we want to eliminate any properties whose intersections reduce to never .最后,我们要消除任何交点减少为never的属性。 That's what 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 extends infer O? { [K in keyof O as O[K] extends never? never: K]: O[K] }: never does. extends infer O? { [K in keyof O as O[K] extends never? never: K]: O[K] }: never It takes the mapped type, copies it to a new type parameter O , and then uses key remapping to filter out any properties whose value type is never .它采用映射类型,将其复制到新的类型参数O ,然后使用键重新映射过滤掉值类型为never的任何属性。

Let's test it on Input :让我们在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;
} */

Looks good!看起来不错!


Note that this only works one level deep (it won't {a: {b: 0 | 1}} | {a: {b: 1 | 2}} into {a: {b: 1}} , for example), and there are probably lots of other edge cases where the above Combine<T> implementation does something different from what you might want.请注意,这只适用于一层深度(例如,它不会{a: {b: 0 | 1}} | {a: {b: 1 | 2}}进入{a: {b: 1}} ) ,并且可能还有很多其他边缘情况,上面的Combine<T>实现做了一些与你想要的不同的事情。 Hopefully you can use this as a starting point.希望您可以以此为起点。 But of course, this sort of type manipulation is very tricky;但是,当然,这种类型的操作是非常棘手的; it's not obvious how and when the compiler decides to treat a union type as a single cohesive type, when it splits it into pieces and forms the result back into a union, and when it splits it into pieces and forms the result back into an intersection.目前尚不清楚编译器如何以及何时决定将联合类型视为单一内聚类型,将其拆分为多个片段并将 forms 的结果返回为一个联合,以及将其拆分为多个片段并将 forms 的结果返回为交集. Seemingly minor alterations will switch from one behavior to another.看似微小的改变将从一种行为转变为另一种行为。 These behaviors are (mostly) documented, but it's easy to get wrong.这些行为(大部分)都有记录,但很容易出错。 So be warned!所以请注意!

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM