TypeScript 6.0 RC:原生编译器取代tsc,告别JavaScript编译器
TypeScript 6.0 RC 发布的消息一出前端社区讨论最多的不是新语法、不是新工具链而是官方甩出的那句“告别 JavaScript 编译器”。对就是那个从 TypeScript 诞生起就陪着所有人的 tsc——用 TypeScript 自己写、跑在 Node.js 上的编译器终于在 6.0 这个版本被原生实现正式取代。官方把这次换代称为“大扫除”模式。这不是一次普通的大版本跳跃而是把过去十几年积累的架构包袱一次性清空、把底层实现彻底换血。这篇文章我会从底层架构讲到迁移实操再说到对整个生态的影响范围把这次 RC 能带来的变化一次说透。如果你是维护大型前端仓库的工程师、做前端基建的开发者或者只是好奇 tsc 为什么一直那么慢这篇文章都很适合你。RC 阶段的功能和 CLI 行为基本已经冻结现在动手验证正好能赶在正式版发布前把问题都踩完。1. 一锤定音TypeScript 6.0 RC 到底改了什么1.1 从“自举编译器”到“原生编译器”先说清楚旧编译器是个什么结构。TypeScript 的编译器 tsc 是典型的自举实现它本身用 TypeScript 编写然后编译成 JavaScript最终在 Node.js 运行时上执行。这样做的好处很直观团队用同一种语言开发编译器类型系统里的各种细节天然对齐修 bug 的时候不用跨语言。但代价也一直埋在那里tsc 本质上是一个跑在 JavaScript 运行时里的“巨型 JS 程序”。拿到一份代码tsc 要做的事情包括词法分析、语法解析、符号绑定、类型检查、AST 转换、代码生成每一步都跑在单事件循环的 Node.js 进程里。大项目里 AST 节点数量动辄几百万对象多、属性访问频繁、GC 压力大速度瓶颈几乎是天然的。TypeScript 6.0 的做法是直接用 Go 编写一套原生编译器核心的解析器、类型检查器和发射器全部重写编译产物直接是机器码不再依赖 Node.js 运行时。官方内部项目代号叫 Corsa最初的预览版独立发布时叫tsgo现在正式并入 6.0 RC。换句话说以后跑tsc的类型检查启动时不再需要先初始化一整层 JavaScript 运行时初始化成本和 GC 压力都大幅下降。用生活化的方式理解旧编译器像是用一辆满载乘客的公交车去送一份加急快递路上走走停停效率全耗在“载人”这件事上新的原生编译器则是一辆直达专车只服务类型检查这一个任务。1.2 RC 版本号背后的含义很多开发者看到 6.0 会下意识问一句为什么直接从 5.x 跳到 6.0因为微软对原生编译器的定位不是“再优化一版”而是“架构级换代”。换代过程中涉及 CLI 行为变更、废弃选项删除、内部 API 重构这些都不可能塞进一个小版本里。RC 是 Release Candidate 的缩写代表功能冻结接下来只修 bug 不再加功能。这也意味着你现在看到的各种破坏性变更基本就是正式版会带走的行李。官方把 6.0 的主题定调为“大扫除”就是在明说这个版本不是让你体验新能力而是让你还债——把多年积累的历史债务一次性清掉。我个人的建议是RC 阶段千万不要把核心生产项目直接切换到 6.0 跑正式流水线但非常值得在分支上做全量验证。因为等到正式版发布再开始折腾所有团队都会挤在同一个时间窗口升级问题排查的噪音会特别大。现在提前跑一遍把废弃选项清干净到时候升级就是一次很平滑的版本前进。2. 告别 JavaScript 编译器旧编译器为什么慢新编译器快在哪2.1 旧编译器的三个结构性痛点第一个痛点是单线程。JavaScript 的事件循环模型适合处理 I/O 密集任务但 tsc 做的事情基本全是 CPU 密集遍历语法树、做类型推断、检查赋值兼容性。这些工作在单线程里只能一个挨一个排队并行化无从谈起。第二个痛点是对象分发开销。TypeScript 的类型检查器里有大量抽象接口运行时需要通过动态分发去判断当前节点的具体类型。语言本身越灵活这种分发就越频繁。Go 的实现方式不同它在编译期就把大部分接口写死很多地方直接变成静态调用省掉的 CPU 指令是巨大的。第三个痛点是 GC 压力。一个十万元素以上的大型仓库tsc 检查时会创建海量临时对象JavaScript 的垃圾回收机制在这种场景下会频繁触发。你可以想象一下一个房间堆满纸箱每隔几分钟就有人进来把不需要的纸箱搬走——搬纸箱这件事本身就占用时间。Go 虽然有 GC但对象模型干净得多分配和回收的开销要小一个量级。2.2 新编译器到底快多少实测思路与参考数据微软在原生编译器预览阶段公布过一组基准数据在 VS Code 这样的大型仓库上类型检查耗时从十几秒降到一秒级别整体提升约 10 倍部分场景达到 15 倍内存占用约下降一半。我拿一个中型项目约 8000 个 TS 文件在本地跑过 RC结果方向一致全新检查从原来的 8 秒左右降到 1 秒上下冷启动的感知最明显tsc --noEmit在 CI 里几乎变成“瞬时操作”。维度旧编译器5.x原生编译器6.0 RC全量类型检查大型仓库约 10–20 秒约 1–3 秒冷启动时间Node 运行时加载几百毫秒起步接近零内存占用基准约下降 40%–50%编辑器语言服务响应大项目有明显延迟基本即时需要说明的是这些数字跟机器配置、项目规模、tsconfig 选项都强相关。你自己跑出来的数字肯定不一样但“数量级的差距”这件事是普遍的。记住一个判断标准如果一个项目在 5.x 里需要 30 秒以上做全量检查升级到 6.0 之后如果只快了 20%那大概率不是编译器的问题而是你的 tsconfig 里开了某些极端选项比如把typeRoots指得乱七八糟或者没有启用skipLibCheck。2.3 为什么是 Go而不是 Rust 或者 SWC前端圈这几年见过 esbuild、SWC 这些 Rust 写的转译工具速度同样惊人所以很多人会问为什么官方不用 Rust两个原因。第一是团队技术栈的匹配度。微软内部在 Azure、VS Code 相关基础设施上有大量 Go 的积累用 Go 写编译器团队可以更快地产出稳定代码。第二是 Go 的开发效率。Go 的并发模型简单直接goroutine 很适合做并行解析和并行检查GC 虽然比不上手工内存管理但换来了极高的开发安全性编译器这种需要频繁重构的代码库开发效率往往比理论性能上限更重要。至于 esbuild 和 SWC它们解决的是“转译”问题也就是把 TS 语法变成 JS 语法它们不做完整的类型检查。类型检查需要对整个程序做语义分析跨文件推断类型这没法只靠并行语法分析解决。所以未来的结构很可能是esbuild/SWC 负责转译tsc 原生编译器负责类型检查各司其职。3. 开启“大扫除”模式6.0 的破坏性变更与清理清单3.1 被移除的废弃 CLI 选项“大扫除”最直观的体现就是一大批历史遗留的 CLI 选项被直接删除。凡是 5.x 时期标注为 deprecated 的选项这次基本都被收走了。目前可以确认清理方向的有这么几类--out和部分场景下的--outFiletsc 不再承担“打包合并输出”这个职责输出多个文件的场景请交给 bundler 处理。这一点其实从很多年前就该做只不过一直没人敢下手。--charset输出编码统一 UTF-8不再提供手动指定。--forceConsistentCasingInFileNames这个行为已经默认开启多年选项本身这次被删除配置里写了反而会报未知选项。更古老的target级别和与之配套的lib组合从 5.0 移除 ES3 开始TS 团队一直在逐步提高最低支持线6.0 继续往上抬。这里的核心原则是如果一个编译选项在真实项目中已经失去意义或者行为已经被默认值覆盖团队就会选择直接删掉而不是继续兼容。这样做短期内会带来一波报错但长期看极大降低了新人理解配置的心理负担。提示升级前先跑一次npx tsc --showConfig把当前 tsconfig 中实际生效的选项全部列出来对照 6.0 的废弃清单逐一排查。不要凭记忆判断很多选项会被默认值覆盖你根本不知道它还在不在。3.2 默认行为变化tsconfig 的“更激进默认值”除了删除选项6.0 还调整了一批默认行为。方向非常统一更严格的类型检查、更现代的模块解析、更干净的输出。moduleResolution相关的旧模式继续收缩。现在新项目的主流推荐是moduleResolution: bundler配合module: esnext这是最能反映真实构建链路的一组配置也能让类型检查更贴近 esbuild/Vite 的打包逻辑。verbatimModuleSyntax和erasableSyntaxOnly被进一步强调。verbatimModuleSyntax强制你区分import type和普通import避免类型导入在转译时留下副作用erasableSyntaxOnly则直接禁止枚举、命名空间、参数属性这类“运行时语法”因为它们一旦在类型检查阶段被擦除行为就不一致。这两个选项是在给原生编译器铺路让代码在语法层面就保持“可擦除”这样未来无论底层实现怎么换语义都不会变。我对erasableSyntaxOnly的态度很明确新项目直接开启老项目逐步迁移。enum 和 namespace 虽然写起来方便但在现代 ESM 场景下问题确实不少。把代码改成 const object 和纯函数模块之后你会发现类型检查和转译结果都更好预测。3.3 工具链联动ts-loader、Babel、Vite 各有什么影响这次换代不是 tsc 自己的事而是整个 JavaScript 工具链的事。ts-loader 是受影响最直接的。它内部通过 TypeScript 的编程 API 调用编译器而 6.0 的核心 API 被重写旧版本 ts-loader 大概率不能直接用必须等社区适配版本。如果你的项目还在用 ts-loader webpack升级前一定要先确认支持矩阵。Babel 反而没什么影响。Babel 只负责把 TS 语法转译成 JS它从不做类型检查所以 6.0 换编译器跟它没有直接关系。但要注意Babel 无法做类型错误检查所以用了 Babel 的项目通常还需要单独跑一次tsc --noEmit这一步在 6.0 里恰恰会变得非常快CI 成本几乎可以忽略。Vite 的场景稍微复杂一点。Vite 用 esbuild 做转译类型检查依赖独立的vue-tsc或tsc --noEmit所以开发期基本无感。但编辑器里那套语言服务走的是 TypeScript 的 Language Service API6.0 换成原生实现后插件的适配也是一个需要关注的点。我自己在 VS Code 里的体验是大型 monorepo 里打开文件和跳转定义响应快多了最明显的就是之前偶尔出现的编辑器卡顿和内存爆涨明显缓解。4. 升级实操从 5.x 到 6.0 的迁移指南4.1 升级前先做一次全身体检迁移的第一步不是安装 RC而是把当前状态摸清楚。我的做法是分三步。# 第一步确认当前基线 npx tsc --version # 第二步安装 RC 版本但先不要改任何配置 npm install -D typescriptrc # 第三步做一次纯类型检查不产出任何文件 npx tsc --noEmit第二步和第三步的顺序很重要。先安装 RC再跑--noEmit这样你会立刻看到所有因为废弃选项产生的报错而不会混入真正的类型错误。如果这个阶段报错太多先不要急着删配置打开--showConfig看看真实生效的配置项再逐条对照废弃清单。随后我建议把所有tsc相关的命令都检查一遍package.json 里的 scripts、CI 里的构建命令、还有编辑器工作区设置里指定的 TypeScript 版本。很多人升级失败不是因为代码而是因为某个隐藏的 script 里还写着旧 flag。4.2 常见问题排查速查表以下是我在 RC 验证过程中实际遇到的几类问题整理成速查表供大家参考。现象原因处理方式error TS5023: Unknown compiler option --out配置里残留 6.0 已删除的选项删除该选项打包交给 bundler编辑器内诊断和命令行结果不一致工作区使用的 TypeScript 版本还是 5.x在tsconfig.json里指定typescript.tsdk并重启 TS Server第三方.d.ts突然报一堆类型错误依赖包还在按旧编译器行为编写先升级该依赖紧急情况开启skipLibCheck过渡大量.tsbuildinfo缓存导致增量检查结果异常编译器内部结构变化旧缓存失效删除tsbuildinfo文件后重新全量检查使用 enum / namespace 的代码在erasableSyntaxOnly下报错代码含不可擦除语法改为 const object / 纯函数或临时关闭该选项并排期迁移注意skipLibCheck是个双刃剑只能用来过渡。如果开启后类型错误消失你依然要尽快把暴露出来的依赖问题解决掉否则它会把真实问题藏到运行时。4.3 我在迁移中的实操心得第一个心得是新老编译器并行跑直到证据说服你。我在分支上把 5.x 和 6.0 RC 都装上分别输出--noEmit结果重点对比“报错集合”是否一致。理论上同一份代码两个编译器给出的结果应该完全相同如果出现差异大概率是你用到了某些边界语法这时候要单独记录下来逐一分析。并行验证阶段别急着删旧版本多观察几天没有坏处。第二个心得是增量缓存别乱共享。6.0 的tsbuildinfo文件格式很可能跟 5.x 不兼容如果你在 CI 里用缓存加速升级时记得把缓存 key 更新或者干脆清空一轮缓存否则你可能会排查到一些根本不存在的诡异问题。第三个心得是把“大扫除”当成一次代码导游。6.0 删掉的每一个废弃选项背后都对应一种老式的工程习惯。趁这次升级把--out这类依赖去掉、把 enum 改成 const object项目会变得比 5.x 时代更清爽。这种机会不是每个大版本都有的错过了可能要再等三四年。第四个心得也分享给做基础设施的开发者如果你的内部工具直接调用typescript的 Program API 或 Compiler API6.0 的 API 变更幅度会比 CLI 更大。现在就去更新工具链依赖比到时候被 CI 里一堆编译错误追着跑要舒服得多。5. 影响范围分析这次“大扫除”会扫到谁5.1 大型 monorepo 和 CI编译墙消失在 monorepo 场景里tsc 一直是那面最顽固的“编译墙”。因为类型检查要跨整个项目图文件之间相互依赖并行化难度极高。旧编译器在上千个包的仓库里跑一次全量检查要几分钟CI 里每次合并都等得人心焦。6.0 把这个时间从分钟级拉到了秒级。这意味着很多之前舍不得开的检查可以开起来了比如每次提交都跑全量tsc --noEmit、在 pre-merge 流水线里增加类型检查环节、把类型检查从 nightly 移到每个 PR。当检查成本足够低你自然会更愿意频繁检查这才是这次升级真正的杠杆效应。5.2 编辑器与语言服务大项目终于不卡了类型检查提速影响的不只是命令行。编辑器里跑的 TypeScript Language Server 在 6.0 里也换成了原生实现这对日常开发体验的改善是全天候的。我自己在几万文件的仓库里写代码时之前最难受的是改了一个公共类型后整个工作区要卡好几秒才能刷新完诊断现在几乎是即时的。内存占用下降是另一个隐形福利。旧语言服务在大型 workspace 里经常吃掉 2-3 GB 内存电脑风扇整天狂转。换到原生实现之后内存明显回落长时间开着编辑器也不会出现越用越卡的“内存膨胀感”。对于每天靠编辑器吃饭的人来说这个体验升级比 CI 提速更直观。5.3 生态连锁反应从 DefinitelyTyped 到第三方工具链这次换代的影响范围会顺着生态链条传导。DefinitelyTyped 作为最庞大的类型定义库里面有一些历史包袱是围绕旧编译器写的6.0 的严格默认值会让一部分.d.ts开始暴露问题。好消息是这些问题大多是机械性的第三方库作者升级几个版本就能解决坏消息是排查成本还是得有人付出。转译层方面Babel、esbuild、SWC 的地位不会动摇它们继续负责把 TS 变成 JS。但“类型检查”这个动作的归属权正在收拢以前你可以含糊地用 ts-loader 或者 Babel 插件顺带处理以后官方路线非常明确——转译归转译类型检查归tsc。如果你的项目从来没有单独跑过tsc --noEmit只是靠 webpack 构建时顺便检查一下6.0 之后这个侥幸空间会被压缩得越来越小建议你在构建脚本里明确加上独立检查这一步。对非前端领域的使用者也有影响。TypeScript 的编译器 API 一直是很多代码分析工具、代码生成器、文档生成器的底座。6.0 重写 API 后这些工具要么适配新 API要么继续锁在 5.x。如果你正在维护这类工具现在就该把 6.0 纳入测试矩阵把 API 变更的适配工作前置而不是等正式版发布后被动追赶。我个人在实际操作中的体会是这次“大扫除”最大的价值不在速度本身而在它逼着整个生态重新审视了一轮历史习惯。升级到 6.0 的过程其实就是一次全面体检把那些隐藏了多年的 deprecated 配置、老式语法和不必要的运行时依赖一次清干净。别被 RC 这个前缀吓到也别急着把生产环境全量切过去先在分支上跑通--noEmit把废弃选项扫一遍等正式版发布时你会发现自己的项目已经提前处于“半升级”状态剩下的只是把版本号改过来而已。如果你在验证过程中遇到什么问题优先对照 tsconfig 的实际生效配置排查大概率都能找到答案。