这几个 TypeScript 类型,90% 的人说不出原因

2022-03-03 16:45:37 浏览数 (1)

有这样几个 TypeScript 类型,大家先试着猜下 res 都是啥:

第一个:

传入的类型参数为联合类型 1 | 'a',问 res 是啥

代码语言:javascript复制
type Test<T> = T extends number ? 1 : 2;

type res = Test<1 | 'a'>;

第二个:

传入的类型参数为 boolean,问 res 是啥

代码语言:javascript复制
type Test<T> = T extends true ? 1 : 2;

type res = Test<boolean>;

第三个:

传入的类型参数为 any,问 res 是啥

代码语言:javascript复制
type Test<T> = T extends true ? 1 : 2;

type res = Test<any>;

第四个:

传入的类型参数为 never,问 res 是啥

代码语言:javascript复制
type Test<T> = T extends true ? 1 : 2;

type res = Test<never>;

先记一下自己的答案,接下来我公布正确答案,大家看下猜对了几个。

答案公布

第一个类型 res 是 1 | 2

再来看第二个类型,res 也是 1 | 2

接下来是第三个类型,res 也是 1 | 2

最后是第四个类型,res 是 never

不管答对了几个都没关系,关键是要知道它的原因,接下来我解释下:

原因解释

第一个类型 res 是 1 | 2

有同学可能会说,这里传入的是联合类型呀,也不是 number,为啥结果是这样呢?

因为当条件类型的左边是类型参数时,会有 distributive 的性质,也就是把联合类型的每个类型单独传入求值,把每个的结果合并成联合类型,这叫做分布式条件类型。

这里的 T extends number 的左边是类型参数 T,传入的是联合类型 1 | 'a',所以会把 1 传入求值、把 2 传入求值,最后把结果合并成联合类型,也就是 1 | 2。

再来看第二个类型,res 也是 1 | 2

为啥这里也是 1 | 2 呢, 刚才说的分布式条件类型是针对联合类型的呀?

没错,boolean 其实也是联合类型,所以会把 true 和 false 分别传入求值,最后结果合并成联合类型,所以是 1 | 2。

接下来是第三个类型,res 也是 1 | 2

同学可能会说:哦,我知道了,any 也是联合类型。

错,any 不是联合类型,这里是因为条件类型对 any 做了特殊处理,如果左边是 any,那么直接把 trueType 和 falseType 合并成联合类型返回

最后是第四个类型,res 是 never

咋还出来个 never,不是只有 1 和 2 么?

这里确实也是 TS 的特殊处理,当条件类型左边是 never 时,直接返回 never

你说了这么多,我怎么知道是不是真的呢,万一是你编的呢?

有怀疑很正常,这也是应该有的态度,不过这些确实都是真的,接下来我从源码来验证下。

从 TS 源码解释原因

这里的重点不在如何读 TypeScript 源码上,我就略过过程了,直接给结果。

对如何阅读 TypeScript 源码感兴趣的同学,可以看我之前的一篇文章:《我读 TypeScript 源码的秘诀都在这里了》,或者看我刚上线的掘金小册《TypeScript 类型体操通关秘籍》,小册里会带大家从源码解释各种类型的原理。

先来解释第一个联合类型 条件类型的情况:

代码语言:javascript复制
type Test<T> = T extends number ? 1 : 2;

type res = Test<1 | 'a'>;

TypeScript 在处理到条件类型 Conditional Type 的时候,会设置一个 isDistributive 的属性,根据类型参数是不是 checkType(左边的类型)来设置。

因为 T extends number 的 checkType 是 T,所以这里的 isDistributive 就是 true,也就是它是分布式条件类型。

那么是分布式条件类型会做什么处理呢?

会在求值的时候把每个类型单独传入求值,最后把结果合并。

对应的源码是这样的:

这里都不用解释了,注释都写的很清楚了, T extends U ? X : Y 当传入的 T 是 A | B 时,结果是 (A extends U ? X : Y)|(B extends U ? X : Y)。

这就是分布式条件类型遇到联合类型时的处理。

所以,源码走到了 mapTypeWithAlias 这个分支,就是做每个类型单独传入求值的。

我们从源码验证了分布式条件类型的特性!

接下来再来看第二个类型,当条件类型 boolean 时:

代码语言:javascript复制
type Test<T> = T extends true ? 1 : 2;

type res = Test<boolean>;

前面说 boolean 也是联合类型,这是不是真的呢?

debug 发现它也走到这个分支了。说明条件成立,boolean 是 union 或者 never

验证一下:

这里的 flags 就是每一个位表示一种类型,然后通过位运算的按位与来判断是否是那种类型:

这种方式占用空间小,计算速度快,很多框架都是这样来标识类型的,比如 React。

所以,从结果来看 boolean 是联合类型,也就是 true | false。那自然也会触发分布式条件类型的特性,把 true 和 false 单独传入求值,最后把结果合并。

然后是第三个类型,当条件类型 any 时:

代码语言:javascript复制
type Test<T> = T extends true ? 1 : 2;

type res = Test<any>;

debug 会发现并没有走到 mapType 那个分支,而走了另一个分支:

这就说明条件不满足,any 不是联合类型呀,我们也可以通过 flags 看下:

按位与的结果是 0,也说明了它不是 union 和 never。

那为啥结果还是 1 | 2 呢?

继续往下走,看条件类型的求值逻辑,会发现这样一段代码:

注释是 Return union of trueType and falseType for 'any' since it matches anything,意思是是返回 trueType 和 falseType 的联合类型,因为 any 匹配任何类型。

原因不就出来了么。(不得不说,TS 源码的注释写的真不错)

然后就是最后一个类型,当条件类型 never 时:

代码语言:javascript复制
type Test<T> = T extends true ? 1 : 2;

type res = Test<never>;

为啥直接返回了 never 呢?

debug 会发现 never 也走到 mapType 这个分支了:

啥情况,never 又不是联合类型,咋分呀。

人家条件里确实写的是 Union 或者 Never:

说明肯定对 Never 做了特殊处理,别着急,我们继续往下看。

继续往下走会发现 Union 和 Never 在这里分叉了:

然后 mapType 里对 never 类型直接就给返回了该类型,也就是 never:

这就是为啥结果既不是 trueType、也不是 falseType,而是 never。

至此,我们通过源码来验证了上面说的原因的真实性。

总结

TypeScript 的类型系统有一些特殊的设计:

条件类型当 checkType(左边的类型)是类型参数的时候,会有 distributive 的性质,也就是传入联合类型时会把每个类型单独传入做计算,最后把结果合并返回。这叫做分布式条件类型。

此外,条件类型遇到 never 会直接返回 never,遇到 any 会返回 trueType 和 falseType 的联合类型。

再就是 boolean 也是联合类型,是 true | false。

0 人点赞