TypeScript 编译器的 Go 语言移植详解
背景介绍
目前没有任何工具能够完全替代 tsc
提供的全面类型检测能力
微软今日发布公告:
[...] 我们已经开始对
TypeScript
编译器和工具进行原生移植。这一原生实现将大幅改善编辑器启动时间,将大多数构建时间减少约 10 倍,并显著降低内存使用量。
本文将深入探讨这一重大消息背后的技术细节。
代码库:JavaScript 与原生实现对比
为避免混淆,我将使用以下术语:
JavaScript
代码库:当前的TypeScript
代码库 — 实际上是用TypeScript
编写的。Native
代码库:新代码库。我使用"原生"这一术语是因为TypeScript
团队也是这样称呼的 - 实际上是用Go
编写的。
为何这是重大突破
类型检查是外部工具无法完成的唯一任务:
因此,类型检查同样变得更快是一个重大进展。
项目时间线
当前 TypeScript
版本为 5.8。
TypeScript 6
(JavaScript
): JavaScript
代码库将继续用于 6.x 系列,6.0 版本将引入一些突破性变更以便与原生代码库保持一致。
TypeScript
原始代号:Strada
TypeScript 7
(原生): 一旦原生代码库达到与 JavaScript
代码库的足够对等性,它将作为 TypeScript 7.0
发布。
- 原生代码库代号:
Corsa
两个代码库将长期共存。
需要迁移的内容
了解 TypeScript
生态系统中哪些部分需要迁移是很有帮助的:
- 命令行
TypeScript
编译器 TypeScript
语言服务器(帮助编辑器支持TypeScript
)JavaScript
代码库早于广泛使用的语言服务器协议,因此不使用该协议。新的语言服务器将使用该协议,这应该使编辑器更容易支持TypeScript
。
- 使用
TypeScript
代码库的工具- 与
Go
代码库的交互需要全新的方法:- 内部组件暴露将减少
- 交互现在跨进程进行
- 与
原生版本何时可供公众使用
当前状态:tsc
可用。仍缺少:
JSX
- 通过
JSDoc
的类型 - 构建模式(项目引用)
2025 年中:带有 JSX
和 JSDoc
的 tsc
(不含构建模式)2025 年底:完整 tsc
和语言服务器
来源:"A 10x Faster TypeScript"
项目何时开始
由 Anders Hejlsberg 开发的第一个原型:
- 始于 2024 年 8 月
- 扫描器和解析器花了 1-1.5 个月编写
- 初始方法:手动编写代码
- 后来:自动将
TypeScript
代码转换为Go
代码的工具 - 移植代码效果良好(需要一些人工干预)
- 移植数据结构只能由人工完成 — 因为
JavaScript
的对象(具有灵活类型)和Go
的结构体(具有高度可配置的数据布局)非常不同。此外,它们现在必须在并发环境中工作 — 例如:JavaScript
代码库通过在创建类型时给每个类型一个序列号来排序类型。这种方法在Go
代码库中不适用,因为由于多线程的参与,类型创建的顺序不再是确定性的。
为何选择 Go 而非其他编程语言
TypeScript
团队希望(主要)移植 JavaScript
代码库,而不是用不同的语言重写 — 原因有两个:
- 新代码库必须(主要)是旧代码库的即插即用替代品。而这通过重写很难实现。
- 重写耗时更长。
如果我们看一下用于新代码库的编程语言的要求,其中一些源于移植的决定:
- 支持循环数据结构 —
TypeScript
代码库大量使用。 - 垃圾回收。代码库假定有此功能。
JavaScript
代码库的风格比面向对象编程更偏向函数式,不经常使用类。这种风格类似于Go
的编写方式。
其余要求由性能和易用性(开发者体验)驱动:
- 在所有主要平台上有良好的原生代码支持。
- 语言应简单易学。
- 语言应有良好的工具支持。
- 对内存中数据结构布局的控制。使用
Go
,你可以使用结构体并创建一个结构体数组,只需一次分配(相比JavaScript
中的多次分配)。 - 对共享内存并发的良好支持 — 这是使代码更快的重要元素(下面会详细介绍)。
为何不选择 C#
当被问到"为何不选择 C#
"时,Anders Hejlsberg 提到以下几点:
Go
比C#
更低级。Go
对生成原生代码有更好的支持(包括指定数据结构的布局)。Go
更适合JavaScript
代码库中使用的非面向对象编程风格。
10 倍性能提升从何而来
- 一半的速度提升来自共享内存并发和使用多核。
- 另一半来自原生代码:
JavaScript
必须实时编译;它必须为其对象提供大量灵活性;它不能内联对象(每个数组元素一次分配 vs. 整个数组一次分配);等等。
JavaScript
确实通过 Web Workers
有并发能力,但内存共享非常有限(参见 SharedArrayBuffer
)。
TypeScript
编译有以下阶段:
- 解析:生成抽象语法树 (AST)
- 绑定:为声明创建"符号表",设置控制流图等
- 类型检查
- 发射:代码生成
在原生代码库中,解析和绑定可以独立完成(不需要内存共享)。然后,数据结构是不可变的,可以在线程之间轻松共享。
解析、绑定和发射的速度与使用的核心数量成线性关系。它们共同占总编译时间的约三分之一。
类型检查占剩余的三分之二,不太容易并行化。因此,使用以下技巧:
- 类型检查适用于单个文件 — 它根据需要延迟加载更多信息。
- 技术:运行多个类型检查器,给每个检查器分配部分文件。
- 线程安全需求不高:检查器只共享不可变的抽象语法树。有些工作是重复的,但不多,因为大部分类型信息是本地的。
- 另一方面,线程不能完全独立操作 — 例如,错误消息不应重复且应以确定的顺序显示。
- 使用 4 个检查器(当前硬编码数量),由于工作重复,内存使用增加 20%,但检查速度提高 2-3 倍。
- 注意,这 20% 是相对于单检查器
Go
的 — 而单检查器Go
使用的内存只有JavaScript
代码库的一半。 - 在测试中,使用 8 个检查器仅带来额外 20% 的速度提升(来源)。
原生代码库是否可运行于 WebAssembly
支持 WebAssembly
很重要,因为它能够实现在线 TypeScript
游乐场等用例。实际上已经支持了。
Kevin Deng 编写了一个"TypeScript Go Playground",使用编译到 WebAssembly
的新代码库。
改进 Go
的 WebAssembly
输出大小和性能的工作正在进行中 — 即:它们可以而且将会变得更好。GitHub 上相关讨论:"Go Wasm performance"
结论:一项令人印象深刻的成就
我对 TypeScript
团队能够如此快速地将 JavaScript
代码库移植到 Go
印象深刻。在一个播客中(见下面的"信息来源"),Hejlsberg 说"我在8月开始原型设计"。我没想到他指的是 2024 年,而是 2023 年甚至更早。
这种速度证明了团队方法的巧妙:如果他们重写 JavaScript
代码库而不是移植它,可能需要数年时间,并导致代码库之间的许多不一致性。
Anders Hejlsberg 表达的一个有趣担忧是,随着类型检查变得更快,人们可能会停止尝试编写可以快速计算的类型 — 例如,使用模板字面量类型很容易创建需要大量计算能力的类型。
也许我们最终会获得分析类型性能的工具。类型级调试也似乎很有用。
信息来源
- 视频 "Syntax podcast: Typescript Just Got 10× Faster",由 Wes Bos 和 Scott Tolinski 主持,与 Anders Hejlsberg 和 Daniel Rosenwasser 对话
- 视频 "TypeScript is being ported to Go | interview with Anders Hejlsberg",由 Dimitri Mitropoulos 为 Michigan TypeScript 制作
- 博客文章 "A 10x Faster TypeScript",作者 Anders Hejlsberg
- 视频 "Anders Hejlsberg on TypeScript's Go Port",由 Matt Pocock 主持
- GitHub 讨论: "Why Go?"