mixin interface union in typescript
今天在一个 typescript 类型系统的设计问题上卡了一晚上。
简单来讲:
type origin = { 'typ1': { num: number, str: string, }, 'type2': { num: string, bool: boolean, str: string, },}type Transformer = /* your code here */;// excepttype result = Transformer<origin>// equalstype result = { num: number | string, str: string, bool: boolean,}type keys = keyof result; // 'num' | 'str' | 'bool'
简单说,我想将一个对象的 sub 对象 intersect 在一起,我当然可以直接写 type result = origin['type1'] & origin['type2']
,但我的 origin
可能有很多 sub object,且后续可能要继续增加,如果是别人接手了我的代码,我希望这里是自动化的、关联的。
我在尝试中,实现了对 object tuple 的 intersection,但我很难将 origin
转化为 tuple。
直接使用 origin[keyof origin]
则会得到一个 union type,此时 keyof
这个 union type 只会得到它们的共有 key。
所以我有两个方向,一是将 origin
转化为 sub object 的 tuple,另一个方向是将 union type 转化为 intersection type。
后者有一个解决方案:
type U2I<U> = ( U extends any ? (u: U) => void : never) extends (i: infer I) => void ? I : never;
这个非常巧妙,其实最初 copilot 和 chatGPT 都帮我把这个 type 补全出来了,但我因为看不懂,以为是 ai 的什么坏 case,就没理会。
这玩意利用了 union type 在 conditional type 中的 distributive 的特性,将 union 在第一个 condition 中 map 到多个具有单独类型参数 function,然后再 infer 到函数的参数上,就从 union 变成了 intersection。
为什么这么怪?看这个 case:
type example = U2I<{ str: string } | { num: number }>;// 向前推一步( (param: { str: string }) => void | ((param: { num: number }) => void)) extends (i: infer I) => void ? I : never
相当于将 u1 | u2
=> (u1) => void | (u2) => void
,然后再 infer 到同一个函数的参数上。
我们最终得到的是 (i: infer I) => void
函数中的参数,在上面的例子中,这个函数实际上是将 union 中的两个函数合在了一起。
思考一下,假设 union 中的两个函数的实现分别为:
// first fun of the unionfunction(param: { str: string }) { assert(typeof param.str === 'string')}// secondfunction(param: { num: number }) { assert(typeof param.num === 'number')}
那么将两个函数的实现合在一起,就是:
function(param: /* ignore temporarily */) { assert(typeof param.str === 'string') assert(typeof param.num === 'number')}
对上面这个合并的函数来说,我们要求他的参数类型一定是同时满足 union 中所有函数的参数类型的,所以一定是取交。
于是我们就得到了 intersection { str: string, num: number }
但如果你尝试 U2I
会得到 never
,因为显然一个变量不可能即是 number
又是 string
,这放到嵌套的 object 中也是一样,存在任意冲突时会整体返回 never,所以简单的取交是不可取的。
如何处理冲突?我们希望共有字段能够在内部自动 union,我们首先需要一个工具:
type DistributeForGetValueTypeUnion< U, K extends PropertyKey> = U extends { [key in K]: unknown } ? U[K] : never
它可以从 interface union 中 pick 出指定 key 的类型 union,例如:
type example = DistributeForGetValueTypeUnion<{ a: number, b: string } | { a: string }, 'a'> // number | string
它是这样工作的,根据 distributive 这个特性,{ a: number, b: string }
和 { a: string }
分别被判断是否存在目标 key,若存在则单独取 T[K]
,否则 never
认为该类型不存在,最后会把所有 T[K]
union 到一起。
下面就比较简单了,只需要从所有 key 中全部 maping 到 DistributeForGetValueTypeUnion
从而重建一个 interface 即可。
type Keys<U> = U extends infer T ? keyof T : never;type Mixin<U> = { [key in Keys<U>]: DistributeForGetValueTypeUnion<U, key>}
我们还实现了一个 Keys
类型,它用来获取一个 interface union 中的所有 key,这里也是 distribute 每个 union 元素,然后单独取 keyof
,最后取 union。