为什么 TypeScript 7 的 Go Port 版本会破坏代码检查工具和框架相关工具
解释了为何 TypeScript 7 搭载了基于 Go 的更快编译器,但其 API 却存在不兼容性,从而导致整个生态系统中的代码检查工具、安装过程以及 Vue/Svelte 的升级均出现问题。
编译器已发布,但它本应一同发布的 API 却尚未出现。
微软于 2026 年 7 月 8 日发布了 TypeScript 7.0,其宣传语几乎是不言而喻的:速度提升十倍。该编译器已从 JavaScript 转向 Go 语言,可在多线程环境下运行,原本需要两分钟才能处理的代码库现在甚至在用户切换到另一个窗口之前就已处理完成。几乎所有报道此次发布的新闻简报都以同样的性能测试图表作为开头。
随后开发者们真的运行了 npm install 命令。
他们中的许多人得到的只是一个运行在损坏的工具链之上的快速二进制文件。而且这种损坏并非轻微问题——简单的属性读取就会导致lint工具崩溃;由于依赖范围不匹配,包管理器直接拒绝安装;Vue和Svelte项目则根本无法升级。一个月后,情况依然如此,目前所有有发布日期的计划中也都未安排修复。
这部分内容被忽视了,但实际上它决定了你是否应该现在就进行这次升级。
大家常引用的数字
首先得承认事实:性能提升并非只是营销噱头。微软在发布公告中公布了自己的基准测试数据,这些数据详细到足以被验证。
该团队称,在完整构建过程中性能提升了8倍到12倍,内存使用量反而下降而非上升,这与人们通常预期的权衡关系完全相反。Slack告知微软,其CI流程中的类型检查时间从大约7分半缩短到了1分钟多一点,而编辑器的使用体验也从在当前规模下几乎无法使用变为几秒内即可加载。Canva则表示,在编辑器中看到第一个错误的时间从大约58秒缩短到了5秒以内。
这些都是实实在在的工程成果,是一年持续努力的结果。后续的任何内容都无法动摇这一点。
这只是移植,而非重写
以下是大多数报道都忽略的技术细节,也正是这一细节解释了其他所有现象。
TypeScript 7只是对原有版本的移植,并非从零开始的重写。团队逐个将现有的编译器文件从TypeScript翻译成Go,刻意保留原有的结构和逻辑,以确保类型检查行为保持一致。在6.0版本下能够正常编译的代码,在7.0版本下也应能以相同方式编译。正是通过这种方式,微软才得以推出如此大规模的编译器,同时避免出现大量行为退化问题,这也体现了在移植过程中所展现出的严谨态度。
但实际上,编译器是两个共享同一名称的独立产品。一个是你可以调用的二进制文件 tsc,另一个则是其他工具作为依赖项引入的库。代码检查工具、测试转换器、基于抽象语法树的代码修改工具以及模板检查工具并不会调用 tsc 并解析它输出的文本,而是直接导入 TypeScript,利用它来遍历代码的语法树,并从检查工具中获取类型信息。
该端口完整地保留了第一个产品,却完全没有保留第二个产品。
编译器本身完好无损地被迁移了过去,但所有周边工具所依赖的 API 却并未随之迁移。
发布说明底部隐藏的那行文字
公平地说,微软并未隐瞒这一点。如果你仔细阅读7.0版本的发布公告,在接近三分之二的位置、在关于如何将7.0与6.0同时运行的说明下方,有一句话是整个技术生态圈在7月期间热议的话题:TypeScript 7.0发布时并未附带API。
这确实是一个重大缺陷。微软表示正在开发新的API,并计划在7.1版本中提供它。据该团队称,既然移植工作已经完成,他们现在又把重点放回功能开发上。
官方设定的发布节奏是每三到四个月推出一个新版本。如果按此时间表,7.1版本将在10月左右发布。目前所知的就只有这些——替代API的实际完成时间尚未确定。
从上到下阅读公告,其中的规律就会变得清晰。首先是展示7.0版本性能提升幅度的表格,接着是大公司给出的赞誉之词。直到这时,微软才几乎顺带提到,仍有相当一部分工具生态系统暂时无法在此版本上运行。
这样的排序是刻意为之的编辑选择,并非有意误导他人。但这也正是为何许多团队是通过终端中的崩溃日志而非公告本身才发现API存在的缺陷。
问题12518
发布周里最有用的成果并非基准测试结果,而是一份错误报告。
当新编译器正式公开发布的那天,有人正在将一个 Vite 加 React 的项目从 6.0.3 升级到 7.0.2,于是针对 typescript-eslint 提出了问题 12518,并附上了复现步骤。该报告记录了两种不同的故障现象。
第一种是 npm ci 完全拒绝安装,因为 typescript-eslint 的包元数据规定了某个同级依赖的版本范围,而 7.0.2 不在该范围内。第二种则是对于那些强行尝试安装的人而言,由于代码试图访问该版本中已不存在的编译器 API 属性,导致在构建程序时 ESLint 在 typescript-estree 模块中出错。
维护者们关闭了该问题。并非因为他们不在乎,而是因为没有可行的解决办法。
单独看似乎是在轻视这个问题,其实并非如此。typescript-eslint的维护者自身无法解决此问题——他们所需依赖的组件尚未发布。关闭该问题只是对事实的如实陈述:真正的解决工作应在TypeScript 7.1层面完成,而非在代码检查工具内部。这意味着当前生态系统中使用最广泛的TypeScript工具对其自身的兼容性问题毫无办法。
ESLint核心团队在次日开启了相应的问题,确认会着手解决,但同样因缺少相同依赖而陷入困境。Vue工具组的维护者也处于同样的状况。所有下游项目都因同一个尚未发布的依赖而受阻。
影响范围
任何导入 typescript 并调用其内部功能的包都会面临此问题。具体而言:
- typescript-eslint 以及基于它构建的所有具备类型感知功能的代码检查规则。这类问题会在安装时或首次运行时就明显出现错误,不会被忽视。
- ts-jest 以及任何基于内部编译器功能构建的转换工具。相比之下,这类问题会以较为隐蔽的方式出现,表现为令人困惑的转换错误而非安装失败,而这其实更糟糕,因为看起来像是测试配置的问题而非版本不匹配。
- ts-morph 以及基于它构建的任何自定义代码修改工具。这是风险最高的类别。深度类型分析功能可能会在后台悄悄失效,导致输出结果出现细微错误而非明显的程序崩溃。在对代码库进行任何可能造成破坏的操作之前,务必仔细检查。
ts-loader 仍在使用旧版 API。有人在相关公告下总结了大家的普遍心态:虽然急于升级,但由于大多数项目仍在使用 webpack,且加载器尚未有兼容的 API,因此大家都在等待 7.1 版的发布。仔细看看那份清单上的内容。其中没有什么是小众或特殊的工具——这些都是当今普通前端团队常用的标准工具集。
编译器本身是稳定的,但围绕它的生态系统却不稳定。这两个状态虽然共享同一个版本号,但实际上截然不同。
微软的解决方案:同时安装两个编译器
微软预见了这种矛盾,于是设计了变通方案,而非让各团队自行想办法。他们发布了 @typescript/typescript6 这个兼容包,其中包含了 tsc6 可执行文件,从而恢复了对 6.0 API 的访问权限。这样一来,旧版编译器与新版编译器就可以共存,而不会因为二进制文件名冲突而导致问题。
需要采用这种变通方法的原因是,像 typescript-eslint 这样的工具会通过同级依赖关系,根据包名来解析 TypeScript。因此推荐的解决方案是使用 npm 别名来重定向该名称。
完整的双编译器配置如下:
{
"devDependencies": {
"@typescript/native": "npm:typescript@^7.0.2",
"typescript": "npm:@typescript/typescript6@^6.0.2"
}
}
通过这种方式,你的代码检查工具、测试转换器以及 codemods 仍能像往常一样导入 typescript,而在底层则会自动使用 6.0 版本。同时,运行 npx tsc 时会使用 7.0 版本,这样你就能在最重要的场景——编辑器和持续集成环境中获得性能提升。
这种方法确实有效,值得称赞:它是一个设计精良的解决方案,相关说明出自官方公告,而非由社区自行逆向推导出来的。
尽管如此,这仍然是两个独立的编译器安装版本共存于同一个 node_modules 目录中,需通过别名来关联,而每位新成员都需要了解这一机制;此外还有相关配置,在 7.1 版本正式发布后最终还得进行调整。事实就是如此:这是一笔没有明确偿还期限的技术债务。
第二重陷阱:跳过 6.0 版本的人会面临的问题
除了缺失的 API 外,那些直接从 5.x 版本跳过 6.0 版本的团队还会面临另一重挑战。
TypeScript 7.0 完全采用了 6.0 引入的默认设置,6.0 中出现的所有弃用警告在 7.0 中都会变成致命错误。这些问题会同时出现:
strict现在已设置为默认值。module的默认值现为esnext。
rootDir 的默认值为 ./,而非自动推断值;因此如果您的 tsconfig.json 位于 src 目录之外,就必须明确设置该值,否则编译器会误判源代码的目录结构。types 的默认值是空数组,而非包含所有类型。如果您的代码依赖于已安装的 @types 包中的全局变量,就必须明确指定这些包的名称,或者使用 ["*"] 来恢复旧的行为。target: es5、downlevelIteration、moduleResolution: node、baseUrl,以及 amd、umd 和 systemjs 模块模式。现在使用这些选项都会导致编译错误,没有例外。在那个列表中,团队特别指出rootDir和types是最容易让人措手不及的两个变更,这与实际情况相符。这两种故障模式都会在终端中输出大量错误信息,看起来像是编译器本身出了问题,而非“某个默认值被更改了”这样的提示。正是这种混淆导致问题被当作编译器漏洞来处理,而非通过简单的一行配置修改就能解决。
还有一个针对进行类型级字符串操作的用户的较为温和的变更。现在,模板字面量类型推断会将表情符号之类的字符视为一个整体,而非将其拆分为两个 UTF-16 编码单元。对于大多数使用场景而言,这是个更直观的处理方式;但对于那些刻意按 UTF-16 编码单元而非可见字符来计数的自定义 Length 风格辅助类型来说,则属于破坏性变更。
实际建议是:如果您仍在使用 5.x 版本,不要直接升级到 7.0,应先升级到 6.0。推出这个中间版本的目的正是为了将这类破坏性变更分摊到两次较小的升级中,而非一次性全部施加给您。
此版本真正为谁设计
这一点值得仔细思考一下。
看看那些在 TypeScript 7 正式发布前就对其进行了测试并给出相关评价的机构名单:VS Code 团队、微软自身的 Office、Teams、Power BI、Loop 与 Xbox 相关团队,还有 Bloomberg、Canva、Figma、Google、Linear、Miro、Notion、Sentry、Slack 以及 Vercel。这些项目的代码量都高达数百万行,拥有专门的构建基础设施团队,他们数月来持续进行预览版本的开发,并在正式发布前将发现的问题反馈给上游团队。
对于这种规模的机构而言,这一版本的发布确实改变了工作方式,相关数据也证明了这一点。微软自身的新闻服务团队称,原本用于等待持续集成测试的时间每月可节省 400 小时。过去单次类型检查需要七分钟,如今将时间缩短约八倍后,彻底改变了工程师的日常工作节奏。
现在将这种情况与一个使用 Nuxt 并配备类型检测工具的五人初创团队作比较。他们的类型检查时间已经缩短到 9 秒。升级后虽然还能再节省大约 8 秒的时间,但代价却是 lint 工具链出问题、Vue 模板检查工具根本无法运行,还必须在 package.json 中使用别名来解决这些问题,而后续加入团队的工程师还得负责向他们解释这些设置。
速度提升的程度与代码库的大小成正比,而问题带来的负面影响则完全不会随代码量增加而变化——它始终是固定的成本,无论项目只有十个文件还是千万个文件都是如此。
这一切都并非出于恶意。这只是项目在优化自身能够观察到的反馈时所出现的自然结果。那些大型企业的代码库早已纳入预览计划,因此早在产品发布之前,它们的问题就已经显现且可被量化。而负责维护生态系统的团队——主要是那些致力于开发代码检查工具、集成 IDE 以及构建工具的志愿者们——却处于无法参与决策的位置,他们得到的回报仅仅是一个兼容性补丁以及“7.1版本会修复这些问题”的承诺。
庞大的代码库让这次升级变成了一种意外之喜;而小型代码库则要为同样的固定成本付出努力,却只能获得微不足道的回报。这种不平衡正是问题的核心所在。
截至今日的准备情况检查
在产品正式发布一个月后,目前的状况大致如下。
如果您正在考虑进行这一迁移,风险最低的方法是在 CI 中将 7.0 作为第二个非阻塞类型的类型检查任务与现有的任务一起运行。这样既能获得真实的计时数据并增强信心,又不会让构建过程真正依赖于 7.0。一旦两个任务都能持续正常运行约一周,您就可以开始切换了。
作为早期采用者并不会带来任何好处,但无论如何还是有必要了解可调整的参数:--checkers 参数用于控制并行运行的类型检查工作线程数量,默认值为 4。在资源有限的 CI 运行环境中,将此值降至 1 或 2 通常比保持默认设置更为明智。
大约五分钟内即可重现故障
这些内容都无需盲目相信,也不应该如此——这种故障发生得既迅速又轻微,足以在您触碰任何重要内容之前,在一个临时测试项目中被刻意触发。
请从一个新的 Vite React-TypeScript 框架开始,添加支持类型检查的 ESLint 配置,然后再尝试强制使用更新的编译器:
npm create vite@latest ts7-probe -- --template react-ts
cd ts7-probe
npm install
npm install -D typescript-eslint eslint
npm install -D typescript@7
大多数人会在安装阶段就遇到问题。已发布的 typescript-eslint 包规定的同级依赖范围上限为 6.1.0 版本之前,因此 npm 会直接拒绝该依赖的解析,显示 ERESOLVE 错误而非温和的警告。实际上,后者才是更为宽容的错误处理方式。
如果强行忽略冲突并继续安装,就会出现更严重的后果。在存在不匹配的包的情况下,运行 lint 时 typescript-estree 在构建程序时会崩溃,原因是试图访问某个已无法解析的属性:
TypeError: Cannot read properties of undefined (reading 'Cjs')
at .../@typescript-eslint/typescript-estree/dist/create-program/shared.js
注意那条错误信息里没有提到什么。它既没有提及 TypeScript 7,也没有指出版本不匹配的问题,更没有写“不支持的配置”。这只是原始的内部崩溃信息——这正是问题 12518 中所反映的抱怨:报告者希望看到清晰、易于理解的兼容性错误信息,而非堆栈跟踪,而至今这一请求仍未得到回应。
现在应用之前介绍的别名解决方案,然后重新运行相同的命令。Lint又能正常工作了,因为它在后台悄悄地与 TypeScript 6.0 进行交互,而单独使用 npx tsc 时仍然会调用速度更快的 7.0 版本。
在采用这种设置后,值得分别测量两个版本下的构建时间。应该由你自己的测试数据而非他人的基准结果来决定选择,而且由于你的代码库长度并非两百万行,其时间差异几乎肯定不会像 VS Code 的测试结果那样显著。
我的心得
TypeScript 7 的特殊之处在于,对于它的两种不同解读其实都是正确的,但大多数评测只选择了其中一种。
这是一项重要的编译器工程成果,它每周都能为那些因构建速度过慢而损失时间的团队节省出实际的时间。然而这次发布却只完成了所需功能的一半,还在公告中刻意隐瞒了这一事实,让那些无法决定发布时间的维护人员承担后果。
本应起到重要作用却未能在发布时清晰传达的是:在最上方加一句简明的话——说明此版本适用于构建流程而非工具链,以及它对用户具体意味着什么。从技术上讲这句话确实存在,只是被埋藏在基准测试图表之后的好几页内容之中。
7.1版本旨在填补这一差距。在它实现之前,合理的做法是采取折中方案:在编辑器和持续集成环境中选择无需额外成本的快速方式,而对于那些仍直接调用编译器的工具,则保持原样不变。
相关阅读
- TypeScript 6在通往原生TS 7编译器的道路上的桥梁作用 — 了解TypeScript 6如何更新默认配置、模块解析机制及导入语法,为代码库适配更快、基于Go语言的TypeScript 7编译器做好准备。