第 53 期 - 从 checker.ts 看 TS 源码中的特殊实践与 JS 对 TS 演进的影响
摘要
分析了 TS 源码中 checker.ts 文件的一些特殊实践,如低配版 named parameters、大量使用 var 等,探讨了 JS 对 TS 演进的限制以及相关性能问题。
1. 五万行 all - in - one 的 checker.ts
这部分主要介绍了 checker.ts 这个文件,它将 TS 完整类型系统 5.2 万行逻辑写在一个 TS 文件里,文件大小达 2.92MB。这看起来很暴力,但作者经过查阅资料和阅读实现后有了一些思考并记录下来。
2. 低配版 named parameters
- 在 JS 规范中,推荐用对象传递多个参数然后解构。但在 TS compiler 这种高频调用场景里,这种方式会造成大量内存开销,如导致 type checking 过程中的内存峰值、频繁 gc 和 mem_copy,还会影响 v8 的 inline caches 优化等。
- 所以 TS 采用了低配版用注释表示的 named parameters 方式,像在其他语言(如 moonbit 或 swift)中的标签函数一样,这种方式可以减少不必要的开销。
3. 能用 number 尽量 number
- 原因是在 v8 里,object 和 string 的开销较大,而小整数(如果 SMI tagged pointer 指针自身数值不算开销的话)甚至无开销。
- 像 switch、const enum、enum bitmap flags 等设计中使用 number 能提升性能。
4. 无限制使用 const enum
- const enum 有个特性可以直接 inline 枚举值到函数里变成立即数,能享受极致优化。
- 虽然社区主流意见不推荐使用,部分 ts 维护者也认为这是个错误,但在 TS 源码里大量使用(800 +个 const enum),如果没有这个特性,tsc 可能会慢不少。
5. ESM/CJS 的性能问题:尤其是 export 导出特别多的时候
- 当 export 导出成员太多时,V8 内部会将其处理成 Slow Properties 字典模式。在高频模块内常量被引用大几百万次时,点读查询开销就不能忽视了。
- 而 checker.ts 将所有东西放在一个文件里,没有这个问题,查询时间为 O(1)。
6. ESM 没有 private 导出
- 在 ESM 中没有提供 private export 这种特性,但 TS 项目有时需要这种特性。
- TS 通过
/** @internal */
注解来变相实现,标记为@internal
的东西在生成 d.ts 的时候会被抹去,从而实现外部无法 import 而内部可以随便 import。
7. TS 甚至大量使用 var,而不是用 let 和 const
- 在部分函数中为了性能全用 var,这是因为在 TS 场景下,v8 这类 js runtime 的 TDZ 检查会影响运行性能。
- 例如在生产构建(production build)比开发构建(dev build)快不少的原因之一就与这种使用方式有关。
8. 往 String.prototype.xxx 上注入东西
- 在普通 JS/TS 项目里这种操作不被认可,但对于静态类型语言来说,有时需要拓展基础类型来使用。
- 像在 swift / Go 之类的语言里基于 string / int 来搞出新类型是很常见的操作。
9. 无类编程,推崇组合编程
- checker.ts 的几万行核心逻辑几乎没有 class 和继承,完全通过函数组合的方式来架构代码。
- 这可能是考虑到 class 继承存在潜在性能问题,如在 V8 引擎下的 A extends B 场景,可能会影响引擎 ICs 的优化效果,导致性能下降。
10. 怎么没有用「表驱动」这种所谓的常用「前端设计模式」?
- 源码里很多根据 ast node kind 走不同逻辑是用 if - else if - else 或者 switch 语句,没有使用表驱动(Record<Kind, Fn>的方式)。
- 原因是表驱动无法被 v8 这类 runtime 静态分析优化,而且会慢几十倍,对于基础设施来说不可接受。
11. 基本没有 try - catch
- checker.ts 里通过返回值 + 往 context.xxx 上写东西的方式来指示异常。
- 这一方面是为了性能,另一方面可能是由于没有 checked exception 导致只能这样才能 type checked。
12. 文件多才是大问题 —— 可惜了半成品的 TS namespace
- 在大型 JS/TS 项目中,文件多会导致找东西困难。
- TS 曾经尝试做满血版的 namespace 特性,但由于确定不做运行时改造而放弃迭代,至今还大量使用 namespace 或用 ESM 模拟 namespace 特性。
13. 最后来个暴论:JS 已经严重影响 TS 的演进了
- TS 如果继续死磕 JS/tc39 而放弃做 runtime feature,可能不会再有进一步演进,因为目前 TS 类型系统已经相当完善。
- 像 2024 年 TS 还没有满血版 ADT + 模式匹配,这属于 runtime 特性,而 JS 自身语言特性限制了 TS 的实现,虽然 TS 的 compiler pipeline 架构设计很漂亮和简洁。
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有