21-原理篇:一些特殊情况的说明
课程
1
如何阅读本小册
已学完
学习时长: 2分6秒
2
为什么说 TypeScript 的火爆是必然?
已学完
学习时长: 4分28秒
3
TypeScript 类型编程为什么被叫做类型体操?
已学完
学习时长: 9分16秒
4
TypeScript 类型系统支持哪些类型和类型运算?
已学完
学习时长: 19分27秒
5
套路一:模式匹配做提取
已学完
学习时长: 41分32秒
6
套路二:重新构造做变换
已学完
学习时长: 39分51秒
7
套路三:递归复用做循环
学习时长: 42分22秒
8
套路四:数组长度做计数
学习时长: 27分58秒
9
套路五:联合分散可简化
学习时长: 19分36秒
10
套路六:特殊特性要记清
学习时长: 36分41秒
11
类型体操顺口溜
学习时长: 27分26秒
12
TypeScript 内置的高级类型有哪些?
学习时长: 31分59秒
13
真实案例说明类型编程的意义
学习时长: 34分36秒
14
类型编程综合实战一
学习时长: 25分55秒
15
类型编程综合实战二
学习时长: 29分9秒
16
新语法 infer extends 是如何简化类型编程的
学习时长: 11分14秒
17
原理篇:逆变、协变、双向协变、不变
学习时长: 11分31秒
18
原理篇:编译 ts 代码用 tsc 还是 babel?
学习时长: 19分49秒
19
原理篇:实现简易 TypeScript 类型检查
学习时长: 47分51秒
20
原理篇:如何阅读 TypeScript 源码
学习时长: 19分27秒
21
原理篇:一些特殊情况的说明
已学完
学习时长: 12分39秒
22
小册总结
学习时长: 2分32秒
23
加餐:3 种类型来源和 3 种模块语法
学习时长: 17分6秒
24
加餐:用 Project Reference 优化 tsc 编译性能
已学完
学习时长: 4分47秒
juejin_logo copyCreated with Sketch.

学完了各种套路,做了大量练习之后,各种类型编程逻辑我们都能写了。但是依然会遇到一些难以解释的、令人困惑的点。

这一节就来集中讲一下这些令人困惑的地方的原理。

isEqual 为什么要这样写

前面讲过 isEqual 要这样写:

type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
    ? true : false;

这样才能正确的判断 any:

试一下

这是为什么呢?

其实就是源码里的特殊处理。

xx extends yy 这里的判断逻辑在 checkTypeRelatedTo 这个函数里,里面定义了各种类型之间如何判断相关性。

其中就有两个都是条件类型的情况的处理:

如图,source 和 target 都是条件类型(Conditional Type)的时候会走到这里,然后有这样一段注释:

如果是两个条件类型 T1 extends U1 ? X1 : Y1T2 extends U2 ? X2 : Y2 相关的话,那 T1 和 T2 相关、X1 和 X2 相关、Y1 和 Y2 相关,而 U1 和 U2 相等。

注意,这里 U1 和 U2 是相等的,不是相关。

如果是判断相关性的话,任意类型 extends any 都是 true,但通过构造两个条件类型判断相关性,就可以利用 extends 右边部分相等的性质来判断两个类型是否 equal。

比如 any 和 1,判断相关性的话,肯定是 true,但是判断相等的话,就是 false 了。不过 TS 没有暴露判断相等的方式,只有 extends 这个来判断类型相关性的语法。

这就是为什么我们要这样判断两个类型相等,就是利用了两个条件类型判断相关性的时候会判断右边部分是否相等的这个性质,算是一种 hack 的写法。答案要从源码找。

为什么我调整了下 extends 左右类型的位置,就报错了

前面我们实现过加法,是这样写的:

通过递归构造长度为 Num1 和 Num2 的元组,然后合并成一个新的元组再取长度的方式来实现的。

试一下

有的同学发现把 Length 和 Arr['length'] 对调之后就报错了:

报的错误是无限递归了:

试一下

这是为什么呢,逻辑看起来没啥错误呀?

大家可以先看下这个案例:

声明一个泛型函数,取它的参数类型,结果是 unknown。

有的同学说,这很正常啊,高级类型就像函数调用一样,现在还没调用,没传入参数呢,当然是 unknown。

对,类型编程中如果需要取类型参数做一些计算的时候,默认推导出的是约束的类型,如果没有类型约束,那就是 unknown

上面那个类型把 T 约束为 number,推导出的就是 number:

Add 那个类型把约束写死为具体的数字的时候,就会发现不报错了:

试一下

所以上面 Add 那个类型里取 Num1 和 Num2 传入 BuildArray 做计算的话,其实传入的是 number:

number extends 某个具体的数字自然永远不成立,永远是 false,所以就无限递归了。反过来写就不会有这个问题。

几个条件类型的特殊情况

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

第一个:

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

type Test<T> = T extends number ? 1 : 2;

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

第二个:

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

type Test<T> = T extends true ? 1 : 2;

type res = Test<boolean>;

第三个:

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

type Test<T> = T extends true ? 1 : 2;

type res = Test<any>;

第四个:

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

type Test<T> = T extends true ? 1 : 2;

type res = Test<never>;

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

答案

第一个类型 res 是 1 | 2

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

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

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

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

原因

第一个就是分布式条件类型的特性,联合类型作为类型参数出现在条件类型左边的时候,会把每个类型单独传入做计算,把结果合并成联合类型。这个我们上节还看过源码。

第二个是因为 boolean 也是联合类型,是 true | false,所以也会触发分布式条件类型。这个可以从源码的注释中找到说明,感兴趣也可以调试下源码,判断下 flags。

第三个是条件类型中 any 的特殊处理,如果左边是 any,则会返回 trueType 和 falseType 的联合类型:

第四个其实严格来说也是分布式条件类型的一种情况,ts 处理分布式条件类型的时候对 Union 和 Never 都做了特殊处理:

但是后面走的分支不一样:

可以看到,如果是 never,那就直接返回了。

所以当条件类型左边是 never 的时候,就会直接返回 never。

严格来说分布式条件类型是包含 Union 和 Never 两种情况的,只不过 never 的情况比较特殊,可以单独摘出来讲,平时我们谈到分布式条件类型(distributive conditional type)就是指联合类型 Union 的情况。

总结

这一节我们集中讲了一些 ts 里令人困惑的点:

  • 判断相等是根据“两个条件类型如果相关,那么 extendsType 部分是相等的”这个特性。

  • 类型参数默认推导出的是类型约束的类型。

  • 条件类型中,联合类型、any、never、boolean 都比较特殊:

    • 联合类型有分布式条件类型的特性,会分发传入
    • boolean 也是联合类型
    • any 会直接返回 trueType 和 falseType 的联合类型
    • never 会直接返回 never,严格来说这个也是分布式条件类型的一种情况

这节从源码角度理清了一些情况的原理,如果大家还有一些困惑的点的话可以告诉我,我再补充进来。

留言
Ctrl + Enter
全部评论(13)
hedgehog_boy的头像
删除
全错[流泪]
点赞
回复
草帽Plasticine的头像
删除
为什么IsEqual要用两个函数类型作为T1和T2,还有X1, Y1, X2, Y2的位置为什么是1和2而不是true和false?
1
5
删除
(作者)
原理篇最后那节有讲
点赞
回复
删除
嗯嗯我看了,就是不理解为啥T1和T2的位置一定要用<T>() => T这样的类型呢?
原理篇最后那节有讲
点赞
回复
查看更多回复
三郎mr的头像
删除
关于加减法的例子还可以这样理解,
type BuildArray<
Length extends number,
Ele = unknown,
Arr extends unknown[] = []
>
= Length extends Arr['length']
? Arr
: BuildArray<Length, Ele, [...Arr, Ele]>;

// 报错,
type test1 = BuildArray<number>;

// 不报错,
type test2 = BuildArray<1>;

也就是说上述定义的BuildArray 参数类型,
既允许 用具体的数字调用,如BuildArray<1>, 这种不会报错;
也允许 用number字面量调用,如BuildArray<number>, 这种会报错,因为Length extends Arr['length'] 永远不相关 ;
展开
1
1
删除
永远不相关,就会产生无限调用,因此报错
点赞
回复
三郎mr的头像
删除
赞赞赞
点赞
回复
EEEEEEEEE的头像
删除
前端
相关是什么意思
点赞
2
删除
(作者)
类型和类型是有关联的,比如 1 extends number 是 true,但是他们不相等
点赞
回复
删除
直白点说就是,相关性的判断,是通过extends来判断的,
点赞
回复