TypeScript 7.0 改用 Go 语言作为编译器以实现更高性能
TypeScript 7.0 将 tsc 转向基于 Go 的实现,引入并行检查器、新的 tsconfig 默认设置、Unicode 模板字面量、更严格的 JS 分析功能,同时还存在暂时的程序化 API 缺失问题。
大约十四年来,TypeScript编译器一直能够自行构建。tsc就是将TypeScript编译为JavaScript并在Node上运行的工具。随着代码库规模增长到数百万行,这种自托管的设计便成了瓶颈。
TypeScript 7.0改变了宿主语言。编译器及语言服务从TypeScript转向了Go。微软将这一改动描述为移植而非重写:类型检查结构与6.0版本一致,但执行部分则是采用共享内存并行处理的原生代码。相关测试显示,其速度相比TypeScript 6.0大约提升了10倍。
有一个经常被引用的测试案例可以直观体现这一变化——检查VS Code项目中的代码(约150万行TypeScript代码)所需时间从大约77.8秒缩短到了7.5秒左右。
如果之前的报道使用的是代号Project Corsa(而旧的JavaScript语法树则被昵称为Strada),那么这一版本就是该项目的最终成果。
为何选择Go,又为何要进行移植?
公众通常猜测是Rust。选择Go是因为它与现有编译器的架构相匹配:复杂的图结构、垃圾回收机制以及难以从头重新实现的循环结构。这种一致性使得逐行移植成为可能。
移植的框架比语言品牌更为重要。保持原有架构就能维持原有的规则。那些在6.0版本下已开启stableTypeOrdering且无论是否启用ignoreDeprecations都能通过类型检查的项目,在7.0版本下也应得到相同的结果——只是引擎速度更快,而非类型系统有所改变。
首先进行了生产环境压力测试。在达到这一里程碑之前,该端口已在 Bloomberg、Figma、Google、Slack、Notion 和 Vercel 等平台上的大型系统中运行了一年多。
真正的并行处理
过去,单个 Node 线程限制了吞吐量。原生工作进程消除了这一限制。版本 7 将解析、检查与输出任务分开处理,并增加了相关标志:
--checkers用于设置并行类型检查工作进程的数量--builders可在单仓库项目中与--checkers结合使用,实现项目引用构建及堆栈的并行处理--singleThreaded会将所有任务集中在一个核心上,以便进行调试及基准时间测试
每文件的解析/输出任务量会随着仓库规模和模块化程度的增加而增长;小型单文件应用的提升效果则较为有限。
监视模式也经过了重构。在庞大的 node_modules 结构中,轮询机制会消耗大量 CPU 资源;将 Parcel 的监视器实现为 Go 语言后,不仅降低了成本,还能更快地响应文件修改。
可能让团队犯难的配置变更
TypeScript 6.0起到了过渡作用:新的默认设置和已废弃的功能会以警告形式出现。7.0 版则将这些错误视为严重错误。那些已经使用过 6.0 版的团队已完成大部分调整工作。直接从 5.x 跳到 7.0 需要时间来整理 tsconfig.json 文件。
值得注意的默认值变化:
- 除非被覆盖,否则
strict选项会处于开启状态 module选项设置为esnext,而target选项则选择比esnext更新的稳定版 ECMAScript 标准noUncheckedSideEffectImports选项默认处于开启状态libReplacement选项默认处于关闭状态
stableTypeOrdering 仍会强制启用rootDir 的起始值为 ./types 初始为空列表文档指出,rootDir 和 types 是团队最先遇到的问题,同时也是最容易解决的问题。
如果 tsconfig.json 位于 src 文件夹之上,需明确设置 rootDir 以保持熟悉的输出结构:
{
"compilerOptions": {
"rootDir": "./src"
},
"include": ["./src"]
}
由于 types 不再自动导入 node_modules/@types 下的所有内容,需手动列出所需项:
{
"compilerOptions": {
"types": ["node", "jest"]
}
}
那些之前仅会发出警告的选项现在会直接导致错误(无法再执行任何有用操作):
- 删除
target: es5和downlevelIteration
moduleResolution 值(node、node10、classic)替换为 nodenext 或 bundlermodule 模式,如 amd、umd、systemjs 和 none,改用 esnext 或 preservebaseUrl,以项目根目录为基准来指定 pathsesModuleInterop / allowSyntheticDefaultImports 设置为 falsealwaysStrict 视为始终处于开启状态如果经过多年未进行审查,moduleResolution 仍保持旧的 node 值,那么该文件就是迁移检查清单。
模板字面量类型中的 Unicode
模板字面量类型现在遵循 Unicode 编码点而非 UTF-16 编码单元:
type HeadTail<S> = S extends `${infer Head}${infer Tail}` ? [Head, Tail] : never;
type Result = HeadTail<"😀abc">;
// In 7.0: ["😀", "abc"]
// Previously: ["\ud83d", "\ude00abc"]
旧版编译器能够拆分表情符号的代理对,这种方式与 JS 的索引机制类似,但很少符合作者的本意。现在迭代操作会像 for...of 和 [...str] 那样直接依据编码点进行。那些刻意统计 UTF-16 单元的工具将会出错,而其他所有情况都能得到预期的行为。
更严格的 JavaScript 分析
.js 检查机制过去对 JSDoc 以及 Closure 时代的惯用写法较为宽容。7 版本则进一步向 .ts 的规则靠拢:
- 需要类型的代码段会拒绝纯值——应优先使用
typeof someValue @enum/@class不再享有特殊处理;需声明真正的类或使用@typedef- 纯
?不能作为类型使用——应优先选择any
! 表示断言——需明确写出 Tfunction(string): void 形式已被 (s: string) => void 取代编程 API 的缺失
7.0 版本仍缺乏稳定的编程 API。那些嵌入了编译器的库——如 ESLint 的 TypeScript 集成、ts-morph、手动编写的转换器,以及针对 Vue、Svelte、Astro、MDX 或 Angular 模板的编辑器工具——因此还无法完全切换。微软认为这一缺陷是暂时的,并预计在 7.1 及后续版本中完成 API 的完善。
同时,如果不需要语言服务器插件,则可使用 7.0 版本。例如,Angular 团队可以在编辑器中保留 6.0 版本的同时,使用 7.0 版本的 tsc 工具来快速进行整个项目范围的 CLI 检查。有一个名为 @typescript/typescript6 的兼容性包,它提供了 tsc6 可执行文件,并重新导出了 6.0 版本的 API,从而使两个版本能够共存:
{
"devDependencies": {
"typescript": "npm:@typescript/typescript6@^6.0.0"
}
}
应该升级吗?
如果已经在使用 TypeScript 6.0,那么迁移的工作量很小,但收益很大:只需调整几个 tsconfig 设置,就能获得更快的持续集成速度以及更快速的编辑器启动速度。对于更早的版本,虽然存在兼容性问题,但几乎所有问题都早已被提前告知。
立即安装候选版本:
npm install -D typescript@rc
在编辑器方面,目前已有 VS Code 插件支持 LSP,而且 VS Code 的原生集成也在不断扩展。Visual Studio 可以通过打开的工作区直接识别 7.0 版本,无需额外的安装步骤。
维护者表示,功能版本将恢复以往数月一次的发布节奏,预计7.1版将填补嵌入式API的空白。
最终效果:沿用熟悉的TypeScript规则,以原生代码形式运行。