有这样几个 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。