1. 这次发布的真正主角编译器换血先说结论TypeScript 6.0 RC 并不是一个加了几个新语法糖的小版本更新它是整个 TypeScript 项目从“JS 编译器”走向“原生编译器”的过渡版本。如果你把 TypeScript 过去的十年看作一个用 JavaScript 写成的“翻译工厂”那么 6.0 就是这家工厂第一次公开宣布我们要换掉核心流水线了而且已经开始清理那些因为历史包袱堆了十年的旧文件柜。“告别 JavaScript 编译器”这句话往浅了说是 TypeScript 团队决定不再用 JavaScript/TypeScript 来实现编译器本身往深了说是整个编译架构、内存模型、增量构建策略都要重塑。官方预览项目代号叫“Corsa”在 7.0 会用 Go 原生实现的编译器完全替代现在的tsc。6.0 做的事情是先把地基清理干净把那些阻碍新编译器上车的老配置、老选项、老行为全部标记废弃或直接拆除所以叫“大扫除模式”非常贴切。这跟你日常开发有什么关系关系很大。只要你用过tsc --noEmit跑大型项目或者等到tsc因为内存把 Node 进程顶爆又或者在 monorepo 里等那几分钟的项目引用检查你就能理解为什么编译器换掉是大事。以前我们用strict: true、moduleResolution: bundler这些配置时背后都是 JS 实现的编译器在承担类型检查和语义分析。它本身跑在 Node 的 Event Loop 和堆内存模型上项目一大就容易卡属性访问检查一次要遍历几层上下文整个分析过程被 AST抽象语法树和类型绑定表拖住。换到 Go 写的编译器之后原来的那些瓶颈被整个推翻重来。不过这里要泼一盆冷水6.0 RC 还不会真的让你体验到 Go 编译器的全量速度因为从 6.0 到 7.0 之间官方要分阶段把模块解析、类型检查、声明生成等部分逐渐迁移到新引擎。你拿 6.0 跑项目很多性能变化其实来自清理后的内部数据结构优化而不是完整的新编译器生效。但是如果你在这个时候不去了解新架构的方向、不提前给自己的项目清理配置债到了 7.0 正式发布你会发现自己老的 tsconfig 突然不认识了某个module: esnext的推论行为变了以前写着“符合预期但没人知道为什么”的类型代码开始报错。那时候再来升级成本比现在高得多。所以这篇内容我尽量按照“编译器到底换了什么 - 大扫除清掉什么 - 现在怎么上手体验 - 常见问题怎么排查”的顺序讲清楚。对于刚接触 TypeScript 的开发者你也能从中理解编译器和编辑器之间到底怎么配合为什么tsc和ts-node、vite的预期行为经常对不上这不是玄学是编译器架构决定的。2. 核心变化从 JS 到 Go 的编译引擎迁移意味着什么2.1 为什么必须丢掉 JavaScript 编译器TypeScript 编译器最初用 TypeScript 本身实现跑在 Node.js 上所以它本质上是一个 JavaScript 程序。这样做的好处是自举编译器的源码能反过来用自己编译开发团队调试方便新手也能看懂部分源码。坏处也很明显JavaScript 是动态类型、垃圾回收由 V8 掌控类型分析这种高 CPU 密集、高内存分配的任务跑在这样的运行时上会有一层先天的性能天花板。你可以把 Node 环境里的旧tsc想象成一个大型仓库里的巡库员每检查一个文件都要抱着一摞纸质档案跑回总台查全局索引查完再跑回来然后下一次类型推断可能又要重复跑。在内存里它表现为大量的对象分配、AST 节点常驻、符号表频繁哈希查找。V8 的 GC 收集力度一大编译器就会被停顿拖慢。项目超过几十万个类型定义后tsc --noEmit冷启动动辄几十秒是常见场景增量编译的缓存又没有那么智能模块图一变动很多缓存就失效了。Go 写的编译器不一样。它能直接用原生线程调度多核并行内存布局更紧凑GC 压力远小于 V8对大量结构化数据的处理效率也更高。官方放出的演示里中型项目全量检查从原来的十几秒降到几秒部分场景能到几十倍差距。当然这是理想 demo真实项目会因为复杂的类型体操、装饰器、条件类型通胀而打折扣但数量级的提升是确定的。2.2 新编译器在管线上的改变新编译器不是简单把tsc --watch用 Go 重写一遍而是把编译器管线的很多阶段拆开重新设计。旧编译器里语法分析、绑定、类型检查、代码生成这几个步骤耦合较深难以并行。新架构计划把“语法分析”和“语义分析”彻底分层甚至把类型检查做成一套可缓存的中间表示这样编辑器的语言服务和大批量 CI 检查可以用同一套分析结果。这意味着两个直接影响第一增量编译不再只是tsBuildInfoFile那样的粗粒度文件时间戳缓存而是可以在类型节点级别做复用第二语言服务也就是 VS Code 里你看到的红色波浪线和命令行编译不再分裂。以前经常出现“编辑器里不报错、命令行一编译就挂”的诡异情况就是因为两条管线走了两套不同的内部实现。新编译器里这些逻辑会被统一。这个统一对 monorepo 是最友好的。现在的项目引用Project References需要你手动维护references新编译器有机会在单进程内同时加载多个子项目并共享类型信息不需要每次都跑到子项目的tsconfig里去读输出目录的.d.ts。2.3 6.0 版本里你能看到的迁移影子既然真正完整的 Go 编译器要到 7.0 才交付那 6.0 RC 里能看到什么迁移迹象主要在三块一是tsgo或叫 Native Preview预览门控开始更完整地暴露出来你能在 CI 里用预览编译器跑项目并与正式tsc对比输出。二是很多原本绑定在 JS 运行时的命令行参数被归并不再给内部实现细节留参数开关。三是部分--build模式下的增量逻辑被重构官方明确说这是为了向新引擎的缓存协议过渡。如果你去跑tsc --all之类的参数会发现有些选项已经提示 deprecated这在以前很少见以前的 TypeScript 为了兼容会尽量让老选项一直可用到天荒地老。6.0 反过来了团队明确表达“不再永远背负旧债”的态度凡是阻碍新编译器的选项都会被快速拆除。3. 大扫除模式清理了哪些旧设计3.1 旧语言版本与目标选项的废弃路径从 TS 5.x 时代开始每次发版都在压缩对非常老的环境的支持。6.0 把这个动作提速了。官方明确要进一步降低对 ES5 时代产物的默认支持比如target: es5仍能运行但部分新语法例如显式资源管理、装饰器新行为不再保证在 ES5 输出下兼容如果你还卡在 IE 级别的兼容要求上可能需要停下来仔细评估。module配置的整合也开始收紧。module: esnext和moduleResolution: bundler这条组合在过去两年成为大多数新项目的主流6.0 会把一些与之冲突的旧选项比如module: commonjs配合moduleResolution: node时的某些隐式行为变更默认值。你在 5.x 下写import然后靠tsc自动转成 CommonJS 的一套脑回路在 6.0 里会有更多警告提示。还有importsNotUsedAsValues这类曾经用来控制类型导入擦除的选项也进入废弃通道。因为verbatimModuleSyntax已经能更干净地处理类型导入的场景保留两个重复选项只会让新编译器实现复杂化。3.2 内部 API 与自定义编译管线的切割TypeScript 的编译器 API 一直是一个开放的“半官方”接口很多工具链例如ts-loader、babel/preset-typescript、babel-plugin、以及各种自定义 transformer都基于它做二次开发。6.0 很清楚地把“编译器的内置 API”和“编辑器语言服务 API”隔开大多数原本挂在ts命名空间下的内部函数被挪走或要求显式导入。对普通项目来说这意味着某些自定义 transformer 插件在新版本下可能找不到内部函数。社区里大量用于试验性语法支持的插件比如装饰器旧提案、参数属性变换等会在 6.0 里提前崩溃或行为异常。这不是质量问题而是官方刻意把不稳定的内部实现从“约定俗成的公共接口”里剔出去。如果你维护过这类 transformer建议现在就去查看官方迁移指南把插件改成基于稳定公开 API。3.3 配置继承与 package.json 解析规则的收紧extends配置继承有了更严格的路径解析规则。以前你在 tsconfig 里写extends: company/ts-config而它没有显式给出入口文件时tsc 会按照你自己的猜测去尝试若干扩展名和目录新版会要求必须明确指向.json文件或一个能被exports字段解析到的入口。package.json的types字段解析也有变化。以前如果一个包同时存在types和exports且exports未定义types条件时TypeScript 会综合两个字段做“模糊推断” 6.0 会更严格偏向exports字段找不到types时直接视为无类型声明。这对老式 npm 包是个大坑因为很多包只写了types: index.d.ts却没有在exports里放置types条件5.x 能过6.0 会直接报错找不到声明文件。清理模式就是这样一个一个把模糊地带打开、晒干、封死。你如果不想被“大扫除”扫到自己最好在下个版本之前给项目里的依赖类型声明做一次全面体检。4. 实操体验把 6.0 RC 跑起来并验证清理项4.1 安装 RC 版本与版本回退预案安装很简单用 npm 或者你常用的包管理器npm install -D typescriptrc然后检查版本号npx tsc --version我这里装下来看到的输出是Version 6.0.1-rc这里有个特别重要的提醒RC 版本不是正式版少数依赖 TypeScript 内部 API 的工具链可能无法识别6.0.1-rc这种版本号尤其是一些直接用字符串匹配版本的脚本。建议你先在分支上测试并且把当前项目的typescript固定版本号记下来方便回退npm ls typescript万一后面发现有 break回退就一句命令npm install -D typescript5.9.34.2 一个最小的实测项目配置为了验证 6.0 的清理逻辑我建议你新建一个测试项目包含这几个文件package.json只需要简单的type: module然后装好 RC 版 TypeScript。tsconfig.json直接用官方推荐的新默认配置不要沿用旧项目里的复杂配置。{ compilerOptions: { target: es2022, module: esnext, moduleResolution: bundler, strict: true, verbatimModuleSyntax: true, noEmit: true, skipLibCheck: true }, include: [src] }在src目录下放一个简单的测试文件// src/index.ts import { readFileSync } from node:fs; interface User { id: number; name: string; } function loadUser(path: string): User { const raw readFileSync(path, utf-8); return JSON.parse(raw) as User; } const user loadUser(./user.json); console.log(user.name);这里故意混合了 Node 内置模块导入和接口声明用来验证verbatimModuleSyntax在 RC 下的行为。5.x 阶段该选项已经存在但 6.0 把类型导入擦除的规则更严格了如果你在这个文件里加了这样的代码import { type User, createUser } from ./user; const u: User createUser(); export { User };注意export { User }只导出了类型但没有用export type { User }6.0 对这种写法会直接报 TS1484 之类的错误而 5.x 下可能只是警告。这是“大扫除”很典型的动作把所有含糊的导入导出行为统一到唯一正确路径上。4.3 对比旧编译器无法做到的点我建议你在同一个测试项目里再跑一个实际产生的类型错误然后对比编辑器提示、tsc输出和tsgo预览输出的差异。比如我特意写了一个等值比较的类型错误const a: string | number hello; if (a 42) { console.log(a); }在 5.9 下tsc会按常规逻辑报错编辑器也会提示但错误信息的位置提示和关联类型链相对“粗”。在 6.0 RC 下跑同样的代码错误信息里a的联合类型收窄描述会更细致部分场景下会给出类型来源的链接跟踪。这是内部类型追踪机制改进的直接体现也是你能在 6.0 里“亲眼看到大扫除”的地方之一。如果你还想看更硬核的编译器变化可以去装typescript/native-preview包在里面跑同样的检查感受一下 Go 编译器在冷启动和增量检查上的速度。但请不要决定正式环境直接换用——根据我的实测native-preview 在极端类型体操代码上偶尔会输出和主编译器不一致的错误现阶段只适合做实验对比。5. 常见问题与排查技巧5.1 编译器堆空间不足常见的“内存爆炸”很多前端开发者跑 TypeScript 编译器时遇到最头疼的错误是FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory这个错误本质上不是 TS 6.0 独有的但 6.0 对于大项目的内存管理出现了新特点原因很微妙新版在类型检查阶段更激进地缓存中间结果短期内内存峰值可能反而升高就像你搬家前先买了更多箱子箱子堆在家里时看起来更占地方。等到后续增量机制和新编译器缓存协议完全生效后这个现象会缓解但现在你仍然可能遇到。排查顺序应该是这样的第一步确认是不是自己的 tsconfig 把所有文件都一股脑包含进来了include范围太大会让 tsc 加载很多不需要的.d.ts第二步排查是否有循环类型引用或“强制分布式条件类型”造成了组合爆炸常见于泛型工具类库的深度嵌套最后才考虑增大内存限制NODE_OPTIONS--max-old-space-size8192 npx tsc --noEmit不过我要说一个反直觉的经验很多“堆空间不足”其实不是内存不够而是某些类型定义里有无穷递归。比如一个泛型类内部持有Recordstring, SomeType而SomeType又引用Recordstring, SomeType的数组每次实例化泛型都会生成新的“类型实体”看起来就像内存泄漏。把 Node 内存调整到 16GB 也会崩。真正有效的排查是二分注释法逐步注释掉可疑类型代码看错误有没有突然消失一旦找到引发组合爆炸的源头再重构类型。5.2 编辑器与编译器行为不一致这里必须强调一个基础但高频的问题编译器和编辑器到底有什么区别。编译器tsc是独立于编辑器的命令行工具负责把 TypeScript 转化为 JavaScript 并按 tsconfig 做全量类型检查编辑器比如 VS Code内置了一套语言服务这套服务底层会调用 TypeScript 的编译 API但它只做增量分析和即时报错不做代码 emit。两者共享大部分逻辑但也存在不同步的可能。如果你装了 6.0 RC 的typescript但 VS Code 还继续使用内置的旧版语言服务你就会看到“编辑器里没报错命令行编译挂了”的怪事。排查方法很简单在 VS Code 命令面板里输入“TypeScript: Select TypeScript Version”然后选择当前项目的 node_modules 里的 RC 版本TypeScript: Select TypeScript Version - Use Workspace Version然后重启窗口。如果编辑器报错和命令行一致了说明两者已经同步到同一核心。以后如果再遇到“vscode 找不到编译器”的提示本质就是 VS Code 没能加载到项目里的node_modules/typescript/lib/typescript.js检查typescript.tsdk配置是否正确指向node_modules/typescript/lib即可。5.3 引入了一个不应存在的工具链版本RC 阶段最容易出现的问题是某个工具链依赖typescript的稳定版本。比如ts-loader、vue/tsc、rollup-plugin-typescript2这些工具的依赖约束有时写的是^5.x你用npm install -D typescriptrc覆盖性安装后它们不会识别 RC 版本导致加载失败或内部调用tsAPI 找不到对应函数。我的建议是如果遇到这类问题把要迁移的流程拆成两步。第一步先升级工具链本身到支持 TS 6.0 的版本第二步再切 TypeScript 版本。不要反过来先切 TypeScript再指望旧版工具链能动态适配。具体到某工具你可以直接去它的 Releases 页面看它是否声明支持rc版本。5.4 声明文件找不到exports 字段导致的心智冲击这是 6.0 大扫除里最容易被低估的坑。过去很多第三方包在发布时只写了顶层types字段而 6.0 对exports字段的解析更严格后这些问题会被集中暴露出来。打开你的项目跑一次tsc --noEmit如果报错集中在node_modules下的包找不到类型声明先看是不是exports问题。解决手段有三个层次。最干净的做法是给项目根目录加一个types补丁文件把你依赖的包的模块声明补齐declare module legacy-package { export default function legacy(): void; }第二种是使用paths映射到本地维护的类型声明{ compilerOptions: { paths: { legacy-package: [./types/legacy-package.d.ts] } } }最后才是用any弱化类型但这会破坏整体检查质量不推荐。如果你维护自己的 npm 包务必在package.json的exports字段里加上types条件{ exports: { .: { types: ./dist/index.d.ts, import: ./dist/index.js, require: ./dist/index.cjs } } }这样新老工具的兼容性都会更好。5.5 格式化工具与语言服务的连环套升级到 6.0 后另一个容易翻车的地方是 Prettier 和 ESLint 里面的 parser。Prettier 用的是自己的 TypeScript parser不受 TS 版本影响正常没事。但 ESLint 的typescript-eslint需要与 TypeScript 编译器共享类型信息来做某些规则检查比如no-floating-promises它要求 TypeScript 版本能兼容其 API。如果你更新 TS 后 ESLint 报内部错误请升级typescript-eslint到最新版本然后清一次缓存npx eslint --cache --cache-location .eslintcache --fix如果你用的是pnpm还容易出现一个幽灵依赖问题编译器在 ESLint 的parserOptions.project里被加载了两次一次是项目里的 TS一次是typescript-eslint自己依赖的 TS两个版本不一致会导致TypeScript命名空间冲突。遇到这种情况在 eslint 配置里显式指向你项目里的tsconfig绝对路径然后用overrides指定 parser 的tsconfigRootDir能减少很多莫名报错。6. 关于清理和升级的一个务实验收清单6.0 RC 的“大扫除”不是在跟你讨价还价它更像是一次强迫症式的搬家整理。那些你写在 tsconfig 里的陈旧选项、藏在 node_modules 里几十年没更新的类型声明、某次为了应付报错加上的ts-ignore都会被新编译器的高亮手电筒照出来。这件事不是坏事但建议你不要被动挨打试着现在就主动做一次清理。具体验收动作我建议按这个顺序做用npx tsc --showConfig输出当前所有解析后的配置项逐条确认是否都认识。搜索项目里所有// ts-ignore和any逐个评估能不能换成更精确的类型。使用新的---explainFiles或者依赖分析工具找出那些被意外包含进编译上下文的.d.ts大文件。在 CI 里加一个tsc --noEmit作为硬性门槛任何类型错误都不允许通过。升级前把typescript版本固定在~5.9.0然后用 RC 单独跑一个分支对比输出差异。如果这一步做好了等 7.0 正式版发布、Go 编译器完全接通时你会发现自己的项目几乎不需要改动就能秒切。这省下来的时间远比你现在提前 30 分钟清理配置要值钱得多。我个人在实际操作中的体会是这次版本换代对老项目来说最痛的不是语法变化而是心态变化——以前很多 TS 配置是可以“蒙混过关”的比如moduleResolution写错一个值不会立刻崩但新编译器不允许它只接受一套明确而严格的规则。这像是从“能跑就行”过渡到“跑之前先证明自己正确”。与其担心升级破坏现有构建不如把它当成一次把项目里所有无关类型债务一次性结清的机会。最后分享一个小技巧如果你在迁移时发现某个错误信息很陌生不知道该按哪条路改先用--pretty false关掉带颜色的输出错误信息里往往藏着源文件路径和依赖链。然后再去官方迭代计划里搜这条错误码你会发现 6.0 对很多老错误码都做了更直白的重写。认准错误码比在网上搜“TypeScript 6 报错怎么解决”要靠谱得多。
