联合类型与交叉类型

联合类型(Union Types)与交叉类型(Intersection Types)在 TS 中都属于很基础的概念,我们也知道联合类型是取并集,用符合(|),交叉类型是取交集,用符号(&)。
但是实际效果和我们直观认知是不一致的,比如如下的代码,有一个 Demo1 类型和 Demo2 类型,Demo3 类型是 Demo1Demo2 的联合类型。

type Demo1 = {
  a: string;
  b: string;
};
type Demo2 = {
  b: string;
  c: string;
};

type Demo3 = Demo1 | Demo2;
const demo3: Demo3 = {} as any;

demo3.a; // ERROR

按照我们对并集的理解,demo3 的属性应该是 demo1demo2 的并集。所以 demo3 里应该有 a, b, c
但是我们实际敲一下代码试一试,却发现不是这样的,我们只能使用 demo3.b 这一个属性。
相反,如果我们使用的是交叉类型,却可以访问所有的成员:

type Demo4 = Demo1 & Demo2;
const demo4: Demo4 = {} as any;

const { a, b, c } = demo4;

为什么会和我们直观感受正好相反呢?其实我们要弄清楚,TS 中的取交集和取并集究竟是对什么来说的。其实 TS 中所有对类型的计算,都是针对使用这个类型的对象来说的。
拿上面那个联合类型的例子来讲,取并集是对所有为 Demo1Demo2 的对象取并集。这样的结果就是这个集合里即可能有 Demo1(有 ab),又可能有 Demo2(有 bc)。而上一讲我们讲过,TS 要保证的是所有成员始终可用,那么也就意味着我们只能用 b
我们可以举个实际的例子来看得更清楚一点,比如我们定义两个类型,一个表示蔡徐坤的粉丝,一个表示马保国的粉丝。

  • 蔡粉
    • 鬼畜
    • rap
    • 篮球
  • 马粉
    • 鬼畜
    • 五连鞭
    • 太极
    • 讲武德

下面我们说:菜粉的举左手,马粉的举右手。然后刚才举过手的站出来(联合类型)。这个时候站出来的肯定既有蔡粉又有马粉。这个时候我们能说站出来的打个太极吗?肯定是不行的,因为蔡粉不会打太极。我们只能说站出来的来段鬼畜,因为蔡粉和马粉都会鬼畜。
相反的,如果我们说,菜粉的举左手,马粉的举右手。然后两只手都举着的站出来(交叉类型)。这个时候站出来的肯定既是蔡粉也是马粉。如果我们说站出来的打个太极,肯定是没问题的,因为他们肯定是马粉,肯定会打太极。如果我说站出来的来段 rap,也是没问题的,因为他们肯定是蔡粉,肯定会 rap。
所以其实知识并不复杂,我们只要能够理解这一点的话,就不用再去记什么乱七八糟的规则了,所有的现象都可以解释了,比如我们随便来写一个例子:

type Test1 = 'a' & string

这是两个原始类型,得到的 Test1 的结果就是 a,我们还拿坤坤来举例,坤坤的粉丝举右手(string),坤坤的超级粉丝举左手(a)。然后两只手都举起来的站起来。这个时候两只手都举起来的,肯定是坤坤的超级粉丝。所以这里 a 是字符串的一种特殊情况,所以 Test1 的结果就是 a
再看一个例子

type Test2 = 'a' & 'b'

最终 Test2 的结果是 never,不存在。因为它不可能同时是 a 又同时是 b。就好比说坤坤的粉丝举左手,坤坤的黑子举右手。然后两只手都举起来的站起来,那么会有人站起来吗,肯定不会。所以是 never
再看一个例子

type Test3 = 'a' | 'b' | 1 & string

这边就涉及到运算了,当联合类型跟其他进行运算时,是分开进行运算的,上面的例子其实等效于:

type Test3 = ('a' & string) | ('b' & string) | (1 & string)

我们知道 'a' & stirng 最终的结果是 'a''b' & string 的结果是 'b'1 & string 最终的结果是 never
所以 Test3 最终的结果是 'a' | 'b'
最后我们看一个复杂的例子,大家可以试着能否根据本期所学的知识,结合上期的协变与逆变,来读懂这个类型函数的作用。

type UnionToIntersection<T> = 
  (T extends any ? (x: T) => any : never) extends
  (x: infer R) => any ? R : never