• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • TypeScript 类型体操通关秘籍

    • 1.如何阅读本小册
    • 2.为什么说 TypeScript 的火爆是必然?
    • 3.TypeScript 类型编程为什么被叫做类型体操?
    • 4.TypeScript 类型系统支持哪些类型和类型运算?
    • 5.套路一:模式匹配做提取
    • 6.套路二:重新构造做变换
    • 7.套路三:递归复用做循环
    • 8.套路四:数组长度做计数
    • 9.套路五:联合分散可简化
    • 10.套路六:特殊特性要记清
    • 11.类型体操顺口溜
    • 12.TypeScript 内置的高级类型有哪些?
    • 13.真实案例说明类型编程的意义
    • 14.类型编程综合实战一
    • 15.类型编程综合实战二
    • 16.新语法 infer extends 是如何简化类型编程的
    • 17.原理篇:逆变、协变、双向协变、不变
    • 18.原理篇:编译 ts 代码用 tsc 还是 babel?
    • 19.原理篇:实现简易 TypeScript 类型检查
    • 20.原理篇:如何阅读 TypeScript 源码
    • 21.原理篇:一些特殊情况的说明
    • 22.小册总结
    • 23.加餐:3 种类型来源和 3 种模块语法
    • 24.加餐:用 Project Reference 优化 tsc 编译性能
    • 25.加餐:一道 3 层的 ts 面试题
    • 26.加餐:项目中 2 个真实的类型编程案例
    • 27.加餐:TypeScript 新语法 satisfies:用声明 or 用推导?
    • 28.加餐:JSDoc 真能取代 TypeScript?
    • 29.加餐:一道字节面试真题

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

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

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 : Y1 和 T2 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,严格来说这个也是分布式条件类型的一种情况

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

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
20.原理篇:如何阅读 TypeScript 源码
Next
22.小册总结