跳到正文

目录

TypeScript 用 Go 重写编译器:这次换的是底层,不是语法

TypeScript 用 Go 重写编译器:这次换的是底层,不是语法

TypeScript 7.0 从底层换掉了编译器:tsc 不再是一份跑在 Node.js 上的 JavaScript 程序,而是用 Go 写成的原生二进制。2026 年 7 月 8 日随 7.0 正式发布后,官方宣称在真实代码库上全量构建通常快 8-12 倍,VS Code 一套约 150 万行的代码库,全量构建从 TypeScript 6 的 125.7 秒降到 10.6 秒。

性能提升是真的。但这次改动最值得注意的不是那些数字,而是它的实现路线:移植,而不是重写。Go 版本把类型检查逻辑原样搬了过来,语义与 TypeScript 6.0 逐字节对齐——升级后你项目里"该报的错"和"不该报的错"不会变,变的是出错前要等多久。


为什么换编译器:tsc 的瓶颈在运行时

TypeScript 编译器从诞生起就用 TypeScript/JavaScript 写,一个以"类型检查"为核心价值的工具,自身却跑在动态类型语言之上。对小型项目感知不强,但代码库到了几十万行,等待就变成了每天都要付的税:

  • 运行时依赖:必须装 Node.js 才能跑 tsc
  • 冷启动延迟:JIT 预热之前,编译器大部分时间在暖自己
  • 内存开销:JS 引擎的堆结构对长时间运行的编译器进程不友好
  • 并行化受限:事件循环模型下,多核只用得上一个
维度TypeScript (JS)Go
产物形态依赖 JS 运行时原生机器码,无依赖
启动延迟高(JIT 冷启动)极低(直接执行)
内存占用JS 引擎堆开销大更紧凑,GC 可调
多核利用受限于事件循环Goroutine 原生并发
增量编译受语言架构限制成熟的高效实现

目标很直接:把 tsc 变成一个可直接分发、本地执行、默认就能吃满多核的二进制。性能是表层,真正的收益是让"Incremental"和编辑器体验不再被单线程拖住。


现状:从预览到 7.0 正式落地

这条路线走了一年多,时间线清晰:

时间节点
2025 年 3 月微软宣布原生移植,以 @typescript/native-preview 发布预览
2026 年 4 月TypeScript 7.0 Beta 发布
2026 年 6 月7.0 Release Candidate 发布
2026 年 7 月 8 日7.0 正式发布,tsc 默认就是 Go 二进制

安装方式随之收敛。预览期用 @typescript/native-preview,7.0 起回归到标准包:

npm install -D typescript
npx tsc --version   # 7.0 起指向 Go 实现

功能覆盖到 7.0 时几乎铺满:

功能状态说明
解析 / 扫描done与 TS 6.0 完全一致的语法错误
类型解析 / 类型检查done相同的错误、位置和消息
JSXdone
声明文件 emitdone
Emit(JS 输出)done
Watch 模式done参考 Parcel 的文件监听思路重写
构建模式 / 项目引用done支持并行构建
增量构建done复用 .tsbuildinfo 跳过未变更部分
语言服务(LSP)done基于语言服务器协议,多线程
API待 7.17.0 未发布稳定程序化 API

最后一行是当前唯一的硬缺口:typescript-eslintts-morph、自定义 transformer 这类依赖编译器 API 的工具,暂时只能继续用 6.0。微软给出的时间点是 7.1。


架构:同语义移植,不是推倒重来

逐字节对齐

移植的首要约束是与现有编译器输出完全一致:

  • 解析阶段的 AST 与原版一致
  • 类型检查的错误位置、错误信息完全匹配
  • Emit 阶段的 JS 输出逐字节相同

这靠一套严格的回归测试保证——每个 PR 都会拿 Go 版本的输出与原版 tsc 做 Diff,拦住行为漂移。这也是为什么它能直接标成"行为不变"。

配置项的收窄

7.0 把 6.0 里标记弃用的选项变成了硬错误,逐项核对 tsconfig 是迁移的主要工作量:

  • module(模块格式)的 amdumdsystemjsnone 移除,推荐用 esnext 交给打包器处理
  • moduleResolutionnode10classic 移除
  • targetes5 移除,最低目标是 es2015
  • baseUrl 移除,路径别名统一收进 paths
  • esModuleInteropalwaysStrict 锁定为开启
  • strict 模式默认开启,target 默认 esnext

这些配置绝大多数现代项目本来就没用,或早已迁移。真正被影响的是带着 2021 年前后旧 tsconfig 一路搬过来的项目——它们升级时遇到的多是配置报错,而不是忽然冒出来的类型问题。

Watch 与增量构建

Watch 模式在 7.0 里重做,文件变更后能增量重检查,不再像预览期那样每次全量跑。增量构建则继续依赖 .tsbuildinfo,正确地跳过未变更部分。


一次构建如何穿过这套系统

tsc --build 跑在一个 monorepo 上,看它一步步做什么:

  1. 程序创建:按项目引用的依赖图找到入口文件,递归解析每个模块,建立整棵程序树。
  2. 并行检查:类型检查被拆成多个 worker,共享同一份内存;默认 4 个,可用 --checkers 调,对 --build 下的多个项目还可用 --builders 并行建工程,--singleThreaded 则完全关闭并行(适合受内存限制的 CI 或调试)。结果会汇总成同一份错误列表——错误集合与 6.0 完全一致,只是等得更短。
  3. 增量落盘:检查通过后写出 .tsbuildinfo,记录哪些文件、哪些检查结果没变。
  4. 发射产品:每个文件转成 JS 与 sourcemap,供打包器使用。

下次再跑,只有变更波及的文件会重新检查,其余直接从 .tsbuildinfo 跳过。单文件改动在 monorepo 场景下能从秒级压到百毫秒级。


性能:测什么、能推出什么、不能推出什么

7.0 的数字来自真实代码库,不是合成基准:

代码库TS 6TS 7提升
VS Code125.7 s10.6 s11.9x
Sentry139.8 s15.7 s8.9x
Bluesky24.3 s2.8 s8.7x
Playwright12.8 s1.47 s8.7x
tldraw11.2 s1.46 s7.7x

关于这些数字,有三点要分清:

  • 测的是全量构建,收益来自两件事叠加:机器码本身和共享内存多线程。旧的 JS 编译器在结构上就并行不起来——JS worker 之间只能复制对象图,而类型检查遍历的恰恰是一整张共享的类型图,所以只能单核跑。Go 版本用 4 个 worker 默认共享同一份类型图,把 --checkers 调到 8,VS Code 的构建能从 10.6 秒再压到 7.51 秒(对 TypeScript 6 是 16.7x)。
  • 内存下降幅度比速度温和。官方数据显示各项目下降约 6%-26%,不是"内存减半"那类夸张说法。
  • 不能推出"所有流程都快 10 倍"tsc --build 里绑定 emit 的阶段提升小一些(独立测约 3-4 倍),编辑器打开含错误文件从 17.5 秒降到 1.3 秒以下,是另一条独立指标。小项目(几万行以下)收益通常只有 2-5 倍,多线程的收益在大代码库才明显。

团队实测的数字

微软公布的基准之外,参与预览的公司也报了几组能对上号的数字:

  • Slack:CI 里类型检查从约 7.5 分钟降到 1.25 分钟,合并队列时间省了约 40%
  • 微软某新闻服务团队:切到 7.0 后每月省下约 400 小时等待 CI 构建
  • Canva:编辑器里看到第一个错误从 58 秒降到约 4.8 秒
  • Vanta:最大的几个项目里,有一例提速约 9x

编辑器端的收益同样可量化:新的语言服务(基于 LSP)相比 6.0,失败的语言服务命令减少 80% 以上,崩溃减少 60% 以上——“重启 TS server"这个习惯动作会明显变少。


与原版的关系:最终并入 TypeScript 主仓库,已经实现

按项目 README 的长期规划,typescript-go 最终会并入 microsoft/TypeScript。到 7.0 这一步,这个规划已经落地:

  1. 没有分裂——还是同一个 TypeScript,只是底层从 JS 变成 Go
  2. typescript 这个包本身发布的 tsc 就是 Go 二进制,预览包只是过渡
  3. 版本号延续——没有因为移植凭空冒出"TS 7”,7.0 就是正常递进

过渡期官方给了并存方案:新增兼容包 @typescript/typescript6,提供 tsc6 可执行文件,并重新导出 6.0 的 API。这样工具链可以继续链接老 API,命令行又用上 7.0 的 tsc。做法是用 npm 别名把两个包都装上:

{
  "devDependencies": {
    "@typescript/native": "npm:typescript@^7.0.2",
    "typescript": "npm:@typescript/typescript6@^6.0.2"
  }
}

项目维护了一份 CHANGES.md,记录与 TypeScript 6.0 的有意变更。这些变更经过设计讨论,不是 bug。预览期遇到与原版不同的行为,先查这份文档,很可能是有意为之。


用 Go 表示 TypeScript 的类型系统

TypeScript 的类型系统以复杂著称:联合类型、泛型、条件类型、模板字面量类型。用 Go 实现时,一个常见的误解要先澄清——Go 自 1.18 起就有泛型,但它的泛型模型(类型参数 + 约束)和 TypeScript 的泛型(可实例化的结构化子类型)不是一回事。Go 的原生泛型并不能直接承载 TypeScript 的类型推导,所以 typescript-go 需要自己实现一套类型表示,而不是把 TS 的每个类型映射到一个 Go 泛型。

它的做法是把类型定义为 Go 接口的层次结构。好处是内存布局确定:Go 的 struct 是紧凑的定长分配,没有 JS 对象那种动态属性开销,也没有 V8 的堆压力。对编译器这种要长时间持有大量中间结构的工作负载,这个差别会累积成可观的省内存。

错误消息兼容

原版 tsc 的错误消息格式经过多年打磨,很多工具链依赖特定格式做解析和国际化。Go 版本必须精确复现这些消息,包括位置信息、错误代码(如 TS2322)、建议文本。typescript-go 把错误模板编译成常量,运行时按需填充参数,既保住了格式,也躲开了反复字符串拼接的开销。

源码映射(Source Maps)

编译结果要携带正确的源码映射,调试时才能映射回原始 TypeScript 源码。这部分与 JS 版本完全兼容,由同一套序列化逻辑生成。


为什么是 Go 而不是 Rust

Rust 也能编译成机器码,性能强、内存安全,但微软最终选了 Go。取舍不是"谁更好",而是"谁更适合这个目标":

考量为什么选 Go
先例esbuild 已经证明 Go 能做出极快的 JS 工具链
内存模型Go 的 GC 自己管,不用手写内存管理,编译这种长任务更省心
架构匹配编译器函数式、递归的代码结构,映射到 Go 的习惯用法很自然
并发Goroutine 对 check 这种可并行任务天然友好
团队熟悉度TypeScript 团队在 Go 上有积累
迭代速度Go 编译快、工具链简单,降低移植的维护成本

Rust 在零成本抽象和精细内存控制上更优,但这里的目标是"产出一个与原版行为一致的编译器",不是追求理论最优。选 Go 是服务于这个目标的技术决策。


谁该先用、谁可以等

这个升级的收益和约束都很明确,按团队情况对号入座:

现在就可以上:

  • 类型检查是 CI 瓶颈的团队。tsc --noEmit 在 CI 里从分钟级压到秒级,收益直接叠加在每次提交上
  • 大 monorepo,编辑器打开和补全经常卡顿的团队,语言服务多线程收益明显

建议等一等:

  • 深度依赖编译器 API 的团队——typescript-eslint 的类型感知规则、ts-morph、自定义 transformer,都要等 7.1 的稳定 API
  • 用 webpack loader 或复杂 emit 管道的项目,7.0 没有 API 面可用,先停在 6.0

迁移前在两件事上花点时间:先升级到 6.0 平滑过渡(7.0 把 6.0 的弃用项变成硬错误),再在分支上验证一遍构建,因为 strict 默认开启和移除的 target 可能翻出之前被压住的问题。


相关链接

  • GitHub:https://github.com/microsoft/typescript-go
  • 原生移植公告:https://devblogs.microsoft.com/typescript/typescript-native-port/
  • TypeScript 7.0 Beta 发布说明:https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/
  • TypeScript 7.0 正式发布:https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/
  • VS Code 团队用 TS 7 加速迭代:https://code.visualstudio.com/blogs/2026/06/26/iterating-faster-with-ts-7

参与讨论

使用 GitHub 登录。欢迎补充事实、异议与实践。