TS经典类型体操:联合类型如何转为转交叉类型?需要知道三个点:分配律、逆变位置、逆变和协变

来看一个经典的类型体操问题:如何实现一个 UnionToIntersection?

具体地说,这个问题有三个子问题:联合类型的分配律、 逆变位置、逆变和协变。我们在递归地解决问题的过程中,递归地给出解答。

// test case
type U = UnionToIntersection<{ a: string } | { b: number }> // type U = {  a: string } & { b: number };

注意,输入不能是原始类型的联合类型,因为原始类型的交叉类型是 never

这个时候,就要用到这两个类型与函数的奇妙碰撞了。

联合类型的分配律

我们知道联合类型遵从分配律。当我们将一个联合类型如 { a: string } | { b: number } 传入一个类型 type T<U> 时,type T<{ a: string } | { b: number }> 实际上等价于 type T<{ a: string }> | type T<{ b: number }>

下面这个是官网的解释。

那如果我们把 type T<U> 写成这个样子:

type ToUnionOfFunction<T> = T extends any ? (x: T) => any : never;

即,我们构造一个将传入的联合类型作为参数的函数。我们将上面的 test case 传入这个类型:

type Functions = ToUnionOfFunction<{ a: string } | { b: number }> 

这个时候,结果就变成了:

type Functions =
    | ((x: { a: string }) => any)
    | ((x: { b: number }) => any)

由于分配律,我们得到了两个参数不同的函数的联合类型。

这个时候我们怎么得到交叉类型呢?

锵锵!看下面!

type UnionToIntersection<T> =  ToUnionOfFunction<T> extends (x: infer P) => any ? P : never;

我们将ToUnionOfFunction<T> 解开后便是 (( (x: { a: string }) => any) | ( (x: { b: number }) => any) ) extends (x: infer P) => any ? P : never

在 TypeScript 的这个 PR 中有一句话:

multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

即:在逆变位置的同一类型变量中的多个候选会被推断成交叉类型。

基于这个性质,我们的UnionToIntersection<T>便满足测试用例了。

逆变位置到底是个什么?

首先记住一句话:函数参数是逆变的,而对象属性是协变的。

变量处于逆变位置就是这个变量是一个函数的参数

到底什么是逆变和协变?!

《深入理解 TypeScript》 的 逆变和协变 一节中有详细介绍。

《深入理解 TypeScript》是本好书呀,建议多看看。

OK,这三个问题解决完之后,我们对这个经典问题也算是搞懂了。