Skip to content
Go back

TypeScript 7.0 发布:用 Go 重写的原生编译器,快 10 倍

TypeScript 7.0 正式发布了,这是一个用 Go 重写的原生版本。官方给出的数字是:完整构建普遍快 8 到 12 倍。这不是又一个加几个类型特性的小版本,而是把整个工具链从 TypeScript 自举实现换成原生编译代码的一次大改造,团队为此投入了一年多。

如果你在大型代码库上被 tsc 或编辑器的卡顿折磨过,这个版本值得认真看一下。这篇讲清楚它到底快在哪、怎么装、和现有 6.0 怎么并存、有哪些会咬人的默认变更,以及哪些场景现在还不能用。

快是怎么来的

去年 TypeScript 团队公布过原生移植计划:用 Go 重写一个能吃满现代硬件的原生编译器。移植做得尽量忠实——在保持原代码库结构和逻辑的前提下写新代码,好让两个编译器结果一致、行为兼容。区别在于新代码库带来了原生代码速度、共享内存多线程,以及一批新优化,在完整构建上通常能拿到 8-12 倍的提速。

安装方式和往常一样,通过 npm:

npm install -D typescript

这会给你工作区里新的 tsc 可执行文件(用 npx tsc 运行)。编辑器支持也一起跟上了,新基础建立在语言服务器协议(LSP)之上,VS Code、Visual Studio、WebStorm 等现代编辑器都能用。VS Code 有一个专门的 TypeScript 7 扩展现在就能装,Visual Studio 会根据工作区自动启用。

快了之后实际是什么体验

纸面上快听着好,落到日常是什么感觉?想想 TypeScript 在开发每个环节里出现的地方:打开编辑器、打开一个 TS 文件、跨项目跑一次 find-all-references;开始改代码时期待自动补全弹出、改错了实时看到红波浪线;准备构建时跑 tsc 看有没有报错,再运行生成的代码。

TypeScript 变快意味着上面每一步都被理顺。等编辑器加载完整个项目会感觉近乎瞬时;find-all-references、自动补全、诊断的延迟只剩过去的零头;跑 tsc(尤其是 --watch 模式)时反馈循环收得更紧,迭代比以前快得多。

在真实项目上能直接看到这个差距。下面是几个较大的开源代码库在 TypeScript 6 和 7 上的构建时间:

代码库TypeScript 6TypeScript 7提速
vscode125.7s10.6s11.9x
sentry139.8s15.7s8.9x
bluesky24.3s2.8s8.7x
playwright12.8s1.47s8.7x
tldraw11.2s1.46s7.7x

内存占用同样更省,同一批项目在整个构建过程里的聚合内存下降了 6% 到 26%:

代码库TypeScript 6TypeScript 7内存变化
vscode5.2GB4.2GB-18%
sentry4.9GB4.6GB-6%
bluesky1.8GB1.3GB-26%
playwright1.0GB0.9GB-11%
tldraw0.6GB0.5GB-15%

编辑器里的体验不止完整构建这一项。同一台机器上,在 VS Code 代码库里打开一个有错的文件,从打开编辑器到看到第一个错误,以前要约 17.5 秒,TypeScript 7 下不到 1.3 秒,快了 13 倍以上。

已经在生产里验证过

TypeScript 项目十多年积累了数万个测试,每次 main 分支提交都跑。但 7.0 不是普通版本,团队在测试套件之外还动用了很多资源确认它能上生产。

过去一年,团队和许多内部及外部大团队一起在真实代码库上测 TypeScript 7,反馈压倒性地正面。微软的 Loop、Office、PowerBI、Teams、Xbox 等团队参与了超大代码库的验证;Bloomberg、Canva、Figma、Google、Linear、Miro、Notion、Sentry、Slack、Vercel 等公司也在自己代码库上测过并给了反馈。

数据上也能看出质量变化:TypeScript 7.0 的新语言服务器把失败的语言服务命令减少了 80% 以上,服务器崩溃减少了 60% 以上(相较 6.0)。一些规模化团队的反馈很直接:

和 TypeScript 6.0 并存运行

TypeScript 7.0 目前不带 API。7.1 预计会带一个新的(且不同的)API,但在那之前,团队优先保证 7.0 能和 6.0 并存,供那些仍需要编程式访问编译器的工具使用(比如 typescript-eslint)。

为此他们发布了兼容包 @typescript/typescript6,它提供一个叫 tsc6 的可执行文件,让你能在没有命名冲突的情况下并存安装。这个包还重新导出了 6.0 的 API,于是你可以让 tsc 用 7.0,其他工具继续依赖 6.0。

因为像 typescript-eslint 这类工具期望直接从 typescript 导入,推荐用 npm 别名实现。可以跑:

npm install -D typescript@npm:@typescript/typescript6

或者改 package.json

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

注意这样做只会给你 tsc6 可执行文件。想同时拿到 7.0 的 tsc,再加一个 TypeScript 7 的别名,npx tsc 就会走 7.0:

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

另外,此前大多数人是通过 @typescript/native-preview 装 TypeScript 7 的(每周下载超 850 万次)。之后每日构建会回归标准 typescript 包的 next 标签,用 npm install -D typescript@next 即可。

并行控制:checkers 和 builders

TypeScript 7.0 现在把很多步骤并行执行,包括解析、类型检查和输出。解析和输出这类步骤在文件间基本独立,所以并行能随代码库变大自动扩展,开销很小。但不是每一步都好并行。

TypeScript 7 引入了实验性的 --checkers--builders 两个标志,用来微调类型检查、项目引用构建这类不那么好并行的步骤。还有一个 --singleThreaded 标志可以完全关闭并行,用于调试或资源受限的环境。

类型检查并行

类型检查在文件间的依赖更复杂:多数文件依赖同一批来自依赖项和全局作用域的类型信息,完全独立地跑检查器会浪费计算和内存。另一方面类型检查偶尔依赖程序内信息的相对顺序,所以从头检查必须每次以相同顺序检查相同文件,才能保证结果一致。

为了兼顾并行和一致性,7.0 创建固定数量、各自持有一份世界视图的类型检查 worker。它们可能重复一些公共工作,但对同样的输入文件,永远以相同方式划分并产出相同结果。

默认 4 个 worker,可用 --checkers 调。在核数更多的机器上加大这个数能进一步提速,但通常以更高内存为代价。前面表格用的是默认 --checkers 4;换成 --checkers 8 同一机器上:

代码库TypeScript 6TypeScript 7 (--checkers 8)提速
vscode125.7s7.51s16.7x
sentry139.8s12.08s11.6x
bluesky24.3s2.01s12.1x
playwright12.8s1.16s11x
tldraw11.2s1.06s10.6x

反过来,在核少内存小的机器(比如 CI runner)上,你可能想调低以避免额外开销,最低可设 --checkers 1,等于把类型检查变单线程、消除重复工作。少数情况下改 checkers 数量会暴露顺序相关的结果差异,跨环境固定一个数值能保证大家结果一致。

项目引用构建并行

7.0 不仅能在单个项目内并行,还能同时构建多个项目,用 --builders 标志控制 --build 下同时运行的并行项目引用构建器数量,对多项目 monorepo 特别有用。

--checkers 一样,加大 builders 能提速但更耗内存,而且它和 --checkers 是乘积关系——--checkers 4 --builders 4 允许最多 16 个类型检查器同时跑,可能过头。不同于 checkers,改 builders 数量不会产生不同结果,但项目引用构建本质上受项目依赖图瓶颈限制。

单线程模式

某些情况下强制整个编译器单线程会有用:调试、对比 6 和 7 的性能、外部编排并行构建,或在资源极受限的环境跑。--singleThreaded 标志不仅把类型检查 worker 限为 1,还确保解析和输出也在单线程里完成。

重建的 —watch 模式

TypeScript 7 带了一个彻底重建的 --watch 模式,底层基于 Parcel 打包器的文件监听器,提供高效稳定的跨平台文件监听。

团队移植监听逻辑时遇到过几个坑:Go 标准库没有内置文件监听 API,试过的第三方库在稳定性、性能、跨平台或构建工具集成上各有问题。纯轮询方案能广泛跑起来但计算太贵,尤其是 node_modules 依赖多的大项目。而 Parcel 的监听器虽然好用,却是 C++ 写的、需要完整 C++ 工具链。他们最后把它移植成 Go(加了几个最小的汇编垫片),从直译逐步打磨成能通过移植测试套件的地道 Go 代码,跨平台的资源占用显著改善。

从 5.x 到 6.0 再到 7.0 的行为变更

TypeScript 7.0 在类型检查和命令行行为上兼容 6.0。基本上任何在 6.0 下(开着 stableTypeOrdering、没设 ignoreDeprecations)能干净编译的代码,在 7.0 下应该编译结果一致。

但 7.0 采用了 6.0 的新默认值,并对 6.0 里废弃的标志和写法直接报硬错误。由于 6.0 还比较新,很多项目需要适配这些新行为。建议先采用 6.0 来平滑过渡。默认配置的显著变化有:

rootDirtypes 的变化最可能让人意外,但都好处理。tsconfig.json 在 src 之类目录外的项目,加上 rootDir 就能保持原目录结构:

  {
      "compilerOptions": {
          // ...
+         "rootDir": "./src"
      },
      "include": ["./src"]
  }

依赖特定全局声明的项目要显式列出它们:

  {
      "compilerOptions": {
          // 显式列出你需要的 @types 包(如 bun、mocha、jasmine 等)
+         "types": ["node", "jest"]
      }
  }

变成硬错误、行为置空的废弃项包括:不再支持 target: es5downlevelIterationmoduleResolution: node/node10(改用 nodenextbundler)、module: amd/umd/systemjs/nonebaseUrlmoduleResolution: classicesModuleInteropallowSyntheticDefaultImports 不能设为 falsealwaysStrict 视为 true 不能关;namespace 声明里不能用 module 关键字;import 上不能用 asserts,要改用 with 关键字。

模板字面量类型现在保留 Unicode 码点

7.0 在从模板字面量类型推断时更自然地处理 Unicode 码点:

type HeadTail<S> = S extends `${infer Head}${infer Tail}` ? [Head, Tail] : never;

type Result = HeadTail<"😀abc">;
//   ^
// 7.0 里:["😀", "abc"]
// 以前:["\ud83d", "\ude00abc"]

以前 TypeScript 跟随 JavaScript 的 UTF-16 索引行为,把 "😀" 拆成代理对的两半。技术上和 JS 索引一致,但通常不是人们想要的,还会产生含不成对代理项、语义无意义的字符串字面量类型。对那些刻意建模 UTF-16 码元的类型级字符串工具(比如某些 Length 工具),这是破坏性变更。新行为更接近用 for...of 遍历或 [...str] 展开时的直觉——"😀" 被当成一个单位。

JavaScript 支持的差异

移植时团队顺带重做了 JavaScript 支持,让它和 .ts 文件的分析方式更一致。一些差异:值不能用在期望类型的地方,改写 typeof someValue@enum 不再被特殊识别;单独的 ? 不再能当类型用,改用 any@class 不再让函数变构造器,改用 class 声明;不支持后缀 !;类型名必须定义在 @typedef 标签里;不再支持 Closure 风格函数语法。更详细的 6.0 到 7.0 差异记录在项目的 CHANGES.md 里。

暂时还不能用的场景

有一点要特别说清楚:用 Vue、MDX、Astro、Svelte 的工作流,以及 Angular 里模板内的专门类型检查,暂时还不能用 TypeScript 7。原因是 7.0 还没暴露稳定的编程式 API,那些把 TypeScript 嵌进自己编译器和语言服务的工具(比如 Volar)目前只能依赖 6.0。团队说这是一个阶段性问题,会主动和这些项目的维护者合作解决。

在那之前的建议:Angular 项目可以用 TypeScript 7 通过 tsc 在命令行做快速的全项目错误检测,编辑器仍用 6.0;用 Vue、MDX、Astro、Svelte 的项目暂时继续用 6.0。VS Code 里跑一下”Disable TypeScript 7 Language Server”命令就能回到 6.0。

往后怎么走

7.0 是 TypeScript 项目的一个大里程碑。这次移植是团队一年多的主要工作,7.0 出了之后,团队会回到新功能、易用性改进、更多性能优化,以及为整个生态实现新 API。发布节奏预计和 7.0 之前类似,每 3-4 个月一个有新特性的版本。7.1 已经在路上,会补上生态里的空缺。

如果你手上有被类型检查拖慢的大型 TS 项目,这个版本值得尽早在非嵌入式语言的场景里试。先把 6.0 的默认变更适配好,迁移到 7.0 会顺很多。

参考


Tags


Previous

从 .NET 视角讲清 MCP:它到底解决什么问题

Next

单体还是微服务:给 .NET 开发者的 5 个判断问题