检查模式与运行模式:TypeScript 7 的 Go 编译器如何融入你的工具链
了解 TypeScript 7 自带的 Go 编译器究竟加快了哪些操作,为何 Angular 和 typescript-eslint 等工具表现落后,以及如何将类型检查与代码执行分开。
来源: TypeScript 7 Moved Its Compiler to Go. Here’s What Actually Changes
TypeScript 7用用Go语言编写的原生版本取代了自托管编译器,而相关数据数值之大甚至让人难以分辨究竟哪些部分的速度有所提升。实际运行时的JavaScript代码速度完全没有变化,变化的是编辑器、CI类型检查工具以及单仓库构建工具的速度。与此同时,Node、Bun和Vite都让TypeScript编译器在代码运行与转换过程中变成了可选组件。本指南将这些问题分开阐述,帮助你判断哪些需要升级、哪些可以暂缓处理,以及如何构建一个将类型检查与代码执行视为两项独立任务的工具链。
由于 TypeScript 的广泛应用,正确掌握它显得尤为重要。2025 年 8 月,GitHub 公布的数据显示,按每月贡献者数量计算,TypeScript 已成为该平台最常用的编程语言,其贡献者数量超过了 Python 和 JavaScript,每月有超过 260 万人为 TypeScript 代码做出贡献。这一生态系统基础的变化会波及各种框架、代码检查工具、包管理器以及编辑器,而这些工具的更新速度并不完全一致。
从自托管编译器到原生编译器
在7版本之前,tsc是一个自启动程序。它的源代码为TypeScript,会被编译成JavaScript,而该JavaScript则在Node环境中运行,用于解析、检查并处理其他人的项目。对于编译器而言,自行托管是一种传统的做法,这种设计助力该语言度过了十年飞速发展的时期。不过它的局限性在于JavaScript运行时:仅有一个主线程,其垃圾回收堆并未针对包含大量语法节点和类型的内存结构进行优化,而且在不同工作进程间共享数据的方式也十分有限。
微软的解决方案是直接移植,而非重新编写。Go语言的代码库刻意模仿现有实现的结构与行为,因此类型系统及其语义保持不变。变化的是其底层基础:原生机器码、可共享内存的线程,以及更强的并行处理能力。正因如此,Go语言成了一个实用的选择;它的内存模型和垃圾回收机制让团队能够使用类似的数据结构来转换现有代码,而无需围绕所有权规则重新设计一切。
已公布的测试结果显示,典型的完整构建速度提升了8倍到12倍:
- VS Code的代码库在TypeScript 6时的编译时间为125.7秒,而在TypeScript 7时降至10.6秒。
- Playwright的完整构建时间从12.8秒缩短至1.47秒。
“TypeScript的速度快十倍”是对这些数据的准确概括。但这一表述也容易引起误解,因为它没有说明究竟哪方面快了十倍。
哪些部分会变快,哪些不会
以一段简单普通的类型化代码为例:一个接口以及符合该接口定义的对象。
interface User {
id: string;
name: string;
}
const user: User = {
id: '42',
name: 'Arthur',
};
无论使用何种工具进行编译,浏览器或服务器最终接收到的都是去除了类型信息的纯JavaScript代码:
const user = {
id: '42',
name: 'Arthur',
};
运行时根本无法知道是哪个编译器处理了源代码,也不知道该编译器是用何种语言实现的。它只能看到生成的 JavaScript 代码,而这种输出与之前并无不同。因此,用 Go 重写 tsc 对应用程序的执行速度没有任何影响。如果页面加载缓慢或 API 处理器存在 CPU 瓶颈,TypeScript 7 也无法改变这一状况。
这些改进在开发过程中就能体现出来,也就是 TypeScript 不断运行的阶段:加载项目、计算诊断信息、提供代码补全和查找所有引用功能、监控文件变化,以及在持续集成环境中再次检查构建结果。有一个指标很好地反映了这一点。在 VS Code 仓库中,打开含有错误的文件时,之前需要大约 17.5 秒才会出现第一条诊断信息;而使用 TypeScript 7 后,这一时间缩短到了不到 1.3 秒。新的语言服务器基于语言服务器协议构建,能够使用多线程,而且微软也报告称语言服务器命令的失败率和崩溃情况大幅减少。
因此,正确的理解是:应用程序本身并没有变快,但开发该应用程序的效率提升了。在庞大的代码库或单仓库架构中,每次类型检查节省几分钟、每次编辑器诊断节省几秒,都会改变你一整天所处的反馈循环。尽管没有任何用户能察觉到这一点,但这确实提升了生产力。
TypeScript 7 的其他更新
原生编译器是讨论的焦点,但还有几个子系统也是围绕它重新构建的。
可调节的并行性
解析、代码生成以及部分类型检查功能可以并行运行,而基于项目引用的构建方式则能同时编译多个项目,这在单仓库架构中尤为重要。你可以明确指定类型检查线程的数量:
tsc --checkers 8
或者,对于资源极为有限的机器,或在调试时需要确定性的行为,可以完全关闭并行处理功能:
tsc --singleThreaded
另一个 --builders 标志用于控制同时运行的项目引用构建数量。应将其视为调整参数,而非“全开所有功能”的开关。每个额外的工作进程都会在内存中占用一部分空间,因此如果简单地将工作进程数量与核心数对应,即使 CI 运行器的核心数很多但内存有限,也可能导致运行速度变慢或出现内存不足的情况。在确定某个设置之前,请先在实际运行环境中进行测试。
重新设计的监视模式
--watch功能也经过了重新实现。团队最初尝试使用Go语言中更为常规的跨平台方案,但对于大型项目而言,频繁查询文件系统会带来过高的成本。最终他们将Parcel框架中C++版本的文件监控机制迁移到Go语言,并开发出了新的监控器,旨在提升不同操作系统上的资源使用效率。
新的默认设置及已移除的旧选项
TypeScript 7延续了TypeScript 6过渡期间引入的配置变更:
strict选项默认处于启用状态。module选项的默认值现为esnext。types选项初始为空数组,因此全局类型包不再会自动被加载。target: es5选项已被移除。- 传统的模块解析机制也被废除了。
baseUrl已不再被支持。自Internet Explorer还作为部署选项以来就未被修改过的tsconfig.json很可能需要处理。尤其是types设置的变化,可能会在未列出所依赖的包之前,导致测试全局变量或Node内置对象出现大量“找不到名称”的错误。如果您尚未处理这些默认设置,我们的关于TypeScript 6.0破坏性默认设置的实用迁移指南中有详细说明,这里的修复方法也适用。
缺失的关键:稳定的编译器API
最关键的问题在于 TypeScript 7.0 并未提供:一个稳定的编程 API。对于许多项目而言,这比任何标志位或默认设置都更为重要,因为它决定了编译器周围的工具是否能够正常运行。
为何生态系统无法一夜之间转变
从终端调用编译器与在编译器基础上构建工具,二者之间的关系截然不同。许多框架、语言工具和代码检查工具并不会简单地直接调用二进制文件:
tsc
它们将 TypeScript 作为库加载,并向其请求语法树、解析后的类型信息、语言服务响应以及其他编译器内部数据。该 API 的替代版本预计将在 TypeScript 7.1 中推出,在此之前微软官方支持同时安装两个主要版本。目前这意味着许多团队会在使用命令行进行检查时采用原生编译器,而其编辑器集成和代码检查工具则继续使用基于 JavaScript 的版本。
有多款常用工具会受到影响:
- Angular 的模板类型检查与 TypeScript 深度集成,因此目前 Angular 完整的编辑器支持仍需依赖旧版语言服务。
- 对于 Vue、Astro、Svelte、MDX 以及其他将 TypeScript 嵌入其他文件格式的工具而言,在新 API 推出之前也会面临类似的限制。
这些都不是该端口存在的缺陷。这正是一个真实生态系统中基础工具迁移时的常态:在所有附加组件完成适配之前,引擎早已准备就绪。
工具依赖 TypeScript 的四种方式
typescript-eslint 是理解这一现象的绝佳视角。ESLint 本身并非基于 TypeScript 编译器构建,因此声称“因为 TypeScript 转而使用 Go,所以 ESLint 变得更快”是完全错误的。不过 typescript-eslint 则起到了桥梁作用,它让 ESLint 能够解析 TypeScript 语法,并在涉及类型检查的规则中查询 TypeScript 的类型信息。尽管该项目一直在尝试使用原生实现,但其当前的解析器仍依赖旧版的编译器 API。维护者们显然正在做准备,但要在其中采用 TypeScript 7 并非仅仅更改版本号那么简单。
那个例子揭示了一个重要区别,每当有人说某个工具“使用 TypeScript”时都应牢记。至少存在四种不同的关系:
- 用 TypeScript 编写。该工具的源代码本身就是 TypeScript。其维护者可以通过类型检查加快构建速度,但用户却不会感受到任何变化。
- 执行 TypeScript 代码。像 Node 和 Bun 这样的运行时可以直接接收
.ts文件作为输入,无需借助tsc。 - 转换 TypeScript 代码。打包工具和转译器会逐个文件移除类型信息并重写语法。
- 依赖 TypeScript 的语义特性。具备类型检测功能的代码检查工具、框架语言服务以及编辑器都需要编译器能够理解整个程序。
不同类别的默认端口变化各异。只有最后一类需要编译器 API,也正是这一类必须等待 7.1 版本才能使用。
正在迁移中的包管理器
pnpm 展示了这些类别之间的交互方式。它是一个庞大的 TypeScript 单仓库项目,包含众多包、复杂的 CI 流程以及大量的编辑器分析功能,从理论上讲非常适合受益于 TypeScript 7 的改进。最终它确实会如此。但在那之前,其维护者仍需确保 typescript-eslint 能正常工作,而该代码检查工具的集成仍依赖于旧版的编译器接口。其依赖关系大致如下:
pnpm
↓
typescript-eslint
↓
TypeScript compiler API
↓
TypeScript 6 — for now
还需要明确的是 pnpm 用户将会遇到什么情况。TypeScript 7 并不会让该命令的执行速度更快:
pnpm install
安装包是 pnpm 的运行时行为,无论其源代码由哪种编译器检查,pnpm 都是在 Node 上以 JavaScript 的形式运行的。TypeScript 7 能提升的是开发 pnpm 的体验:类型检查、CI 测试以及其他由编译器驱动的工作流程。即便如此,这种改进的速度也取决于其依赖关系图中最慢的那个环节。一个大型项目无法比其所依赖的工具更快地采用根本性的变更。
立即可见的收益
那些仅需将编译器用作命令行检查工具和语言服务器的项目就能立即受益。VS Code就是最典型的例子:庞大的TypeScript代码库,每天都有数百万开发者在此使用TypeScript相关工具。将完整的编译时间从两分钟以上缩短到大约十秒已经意义重大;而将编辑器首次给出诊断结果的等待时间从约十七秒缩短到略超过一秒,则是每个人都能感受到的变化。
作为微软的基准测试项目之一,Playwright在默认配置下的完整编译时间从12.8秒降至1.47秒。这同样并不意味着Playwright启动Chromium的速度更快,而是意味着维护和检查Playwright自身代码的效率大大提高。鉴于如此多的开发者工具本身就是用TypeScript编写的,这些工具更快速的维护效果会在整个生态系统中产生连锁反应。
Vite 已将检查与转换视为独立的任务
Vite 主要使用 TypeScript 编写,但在服务或构建 TypeScript 应用程序时,并不会调用 TypeScript 编译器来转译每个模块。现代版本的 Vite 会运行专用的转换流程,并有意跳过类型检查。其文档建议将 TypeScript 单独运行,作为一次性检查:
tsc --noEmit
或是在开发过程中持续进行检查:
tsc --noEmit --watch
这个推理是合理的。转换一个文件只需对该文件进行操作:去除注释、重写部分结构,然后即可完成。而类型检查则需要了解整个模块关系图,因为一个文件中的改动可能会破坏另一个文件中的约定。如果在同一步骤中同时进行这两项操作,快速的操作就必须等待慢速的操作完成。因此,典型的 Vite 项目从同一源代码出发会有两条并行处理路径:
┌── Vite / Oxc ── transform ──> Browser
TypeScript source ──┤
└── TypeScript ───── type-check
从这一角度看来,TypeScript 7无需成为最快的 JavaScript 转换工具,因为 Vite 已经拥有专为该任务优化的工具。TypeScript 可以专注于需要理解整个程序的工作,而这项工作的速度现已大幅提升。如果今天就采用这种配置,一个实际后果是类型错误不会导致开发服务器停止运行;请确保在 CI 流水线或提交前的钩子中运行 tsc --noEmit,以避免错误漏过。
除 JavaScript 之外其他语言编写的 JavaScript 工具
这里存在一个更广泛的趋势。Vite 当前的构建流程使用 Rolldown,而 Rolldown 和 Oxc 都是用 Rust 编写的。esbuild 在多年前选择了 Go 语言,Bun 则用 Zig 实现了自身的大部分功能。现在 TypeScript 也加入了使用 Go 语言的行列。
这并非对 JavaScript 的批评。对于 JavaScript 和 TypeScript 开发者而言,公共接口依然使用起来十分自然,而背后那些需要大量 CPU 资源的解析、处理和分析工作则以原生代码的形式执行。不同层次有着不同的需求,TypeScript 7 就是近期最典型的按层次选择实现语言的例子。
无需 tsc 即可运行 TypeScript 的运行时环境
在运行时层面也在出现同样的分化。传统上,人们认为 TypeScript 项目的流程是从源代码直接经过编译器生成 JavaScript,再由 Node 执行:
TypeScript
↓
tsc
↓
JavaScript
↓
Node.js
实际项目中的步骤总是更多,但核心思路不变:Node 能运行 JavaScript,因此 TypeScript 必须先转换为 JavaScript。如今这一流程可以直接实现:
node server.ts
重要的一点是,Node 并不会运行 TypeScript 编译器。它的内置支持被刻意设计得极为有限:只会移除可删除的 TypeScript 语法,然后执行剩余部分。它不进行类型检查,而那些需要生成实际 JavaScript 代码(而非仅仅删除注解)的语法也不在移除范围之内。像这样的带类型声明的代码:
const port: number = 3000;
在执行之前实际上会变成如下形式:
const port = 3000;
“Erasable”是关键字。诸如带有运行时值的enum、namespace构造,或是构造函数参数属性等,都会生成源代码中不存在的JavaScript代码,因此单纯的代码剥离无法处理它们。如果希望代码在类型剥离后仍保持兼容性,TypeScript提供了erasableSyntaxOnly编译器选项来标记这类构造;请查阅当前版本的Node和TypeScript文档,了解具体行为。
这一功能在 Node 26 出现之前就已存在,因此不应将直接执行 TypeScript 描述为 Node 26 的新特性。Node 26 自身带来了诸多变化,包括默认启用 Temporal、更新版的 V8 引擎、Undici 的又一次重大升级,以及每次重大版本发布后惯常进行的代码清理工作,但这些都属于另一个话题。对于本次讨论而言,重要的是发展趋势:Node 越来越倾向于将 TypeScript 视为可直接处理的输入,而无需先让 tsc 生成 JavaScript 代码。
Bun 将 TypeScript 视为普通输入
Bun 仍处于 1.x 版本系列,从早期就开始采用这种方式。运行 TypeScript 文件的操作非常简单:
bun server.ts
Bun 的加载器会使用其自身的工具在执行前剥离并转换 TypeScript 语法。需要再次强调的是,代码转译并不等同于类型检查。因此,一个相当合理的 Bun 工作流程仅需两个命令即可完成:
tsc --noEmit
bun server.ts
第一个问题回答的是“该程序是否符合类型系统要求?”,第二个问题则回答“该程序能否运行?”。现代 Node 也支持这种分离方式:
tsc --noEmit
node server.ts
在这两种情况下,运行时都不会等待类型检查器,而类型检查器也无需生成任何输出文件。需要注意的是,以这种方式运行的项目没有用于检测类型错误的构建步骤;如果该检查未被集成到持续集成流程中,那么这类错误就根本不会被发现。
TypeScript 的职责在于理解程序
多年来,工作流程一直被压缩为单一的管道,编译器恰好位于代码编写与运行之间:
Write TypeScript
↓
Compile TypeScript
↓
Run JavaScript
现代工具越来越类似于从同一源头分出的两个分支,一个用于理解与检查,另一个则用于转换与执行:
┌── TypeScript ── understand and check
TypeScript source ────────┤
└── Runtime / bundler ── transform and execute
Vite 可以利用 Oxc 对代码进行转换,同时由 tsc 进行检查。Node 会自行移除可被删除的语法元素。Bun 则拥有自己的加载器。这些工具都不需要 TypeScript 7 或任何 tsc 就能运行你的应用程序。正因如此,原生编译器依然重要:TypeScript 最独特的功能已不再是像其他工具那样快速将 .ts 文件转换为 .js 文件,它的真正价值体现在以下这类涉及整个程序的问题上:
- 这个值是否与其被传入的契约兼容?
- 代码重构是否破坏了其他包中的使用该值的代码?
- 这个泛型约束是否正确?
- 这个符号指的是什么,这种类型还在其他地方被使用过吗?
- 编辑器在这里应该提供怎样的自动补全建议?
要满足这些需求,需要的上下文远多于仅仅从某一行中删除 : string 这样简单,而整个程序的分析正是 Go 版本大幅提升速度的部分。如需更深入地了解如何在这些工具上原生运行 TypeScript,请参阅我们的TypeScript 到 Go 编译器及原生执行的迁移指南。
关键要点
- TypeScript 7 是将现有编译器移植到 Go 语言上,并非对语言本身的重新设计;生成的 JavaScript 代码以及应用程序的运行性能均保持不变。
- 其优势主要体现在开发过程中:完整构建的速度通常可提升 8 到 12 倍,编辑器诊断速度更快,语言服务器也更稳定。
--checkers、--builders 和 --singleThreaded 参数,而非盲目增加工作进程数量。typescript-eslint 可能会要求在某些集成中使用 TypeScript 6,同时支持两种版本并行运行。tsconfig.json 中的新默认值以及已被移除的选项(strict、module、types、ES5 目标版本、传统解析方式、AMD/UMD 格式、baseUrl)。tsc --noEmit 设为 CI 流程中的必选环节。