首页 / 文章 / TypeScript 6's Bridge Role in the Path to a Native TS 7 Compiler

本文以英文发布。

TypeScriptJavaScriptCompilersNode.jsToolingPerformance

TypeScript 6's Bridge Role in the Path to a Native TS 7 Compiler

Learn how TypeScript 6 updates default configs, module resolution, and import syntax to prepare codebases for the faster, Go-based TypeScript 7 compiler.

2201 词

每一位维护庞大 TypeScript 代码库的开发者都经历过类似的延迟现象:当你点击保存时,编辑器会暂停几秒钟,等待语言服务器处理完成。在 CI 流水线中,由于仅类型检查就需要五到十分钟的时间,拉取请求会不断堆积。

开发者保存文件 ──> [基于 JS 的 tsc:单线程处理] ──> 反馈缓慢
开发者保存文件 ──> [TS 7 原生引擎:并行线程] ──> 即时反馈

TypeScript 7 引入了一个用 Go 语言编写的原生编译器。它可以通过多线程处理任务,将构建时间缩短8到10倍。不过,你不能简单地将这个多线程原生编译器引入那些仍依赖2018年遗留配置选项的项目中。

这正是 TypeScript 6 的作用:它充当了一个过渡点。它能清除过时的技术债务,更新陈旧的默认设置,确保在 TypeScript 7 推出后项目仍能顺利编译。

为什么 TypeScript 需要过渡版本?

十多年来,TypeScript 编译器本身是用 TypeScript 编写的,并在 Node.js 上运行。这使得 JavaScript 开发者能够直接为编译器的代码库做出贡献。

问题在于 JavaScript 的执行是单线程的。随着代码库发展成包含数百万行代码的庞大单体仓库,编译器的性能最终遇到了瓶颈。

TypeScript 7 通过让多个 CPU 核心并行执行原生机器码来解决这一问题。然而,平行化的编译器带来了两个此前并不重要的行为要求:

  • 确定性排序:即便多个核心同时处理类型检查,编译器也必须确保报告的错误和推断出的类型始终以一致、可重复的顺序出现。
  • 与现代标准的对接:继续支持诸如AMD这样已有数十年历史的模块系统,或是过时的解析策略,会给原生编译器带来不必要的复杂性与性能开销。

TypeScript 6标志着团队开始严格执行这些标准。它引导开发者遵循当前的ECMAScript规范,从而避免在升级到7版本时出现混乱。

tsconfig.json中最大的配置变化

TypeScript 6 引入的大多数可见变化都体现在 tsconfig.json 文件中。一些长期存在的默认值已被更新,以反映当今项目实际的构建方式。

1. strict: true 现已成为默认值

此前,如果 tsconfig.json 文件为空,则 TypeScript 会以宽松模式运行,除非用户手动设置 "strict": true

从 TypeScript 6 开始,严格模式会自动启用。

// tsconfig.json
{
 "compilerOptions": {
 // 在 TypeScript 6 中这已默认启用
 "strict": true
 }
}

如果你的项目已经将 strict: true 设置为默认值,那么无需做任何更改。但如果你的代码依赖于隐式的 any 类型或未加保护的 null 和 undefined 值,升级后就会立即出现类型错误。

为何这很重要:

当类型信息明确且一致时,原生类型检查器才能最高效地工作。宽松的类型定义会迫使编译器处理各种不可预测的情况,从而降低静态分析的速度。

2. 模块目标默认为 esnext

历史上,TypeScript的默认输出目标为ES3或ES5等较旧版本。实际上,如今用于生产环境的几乎所有运行时都是长期支持的——Node.js、Bun、Deno以及现代浏览器都支持原生的ECMAScript模块。

从TypeScript 6开始,默认的module选项变为esnext,而默认的target则指向当前版本的ECMAScript标准。

// 推荐的现代配置
{
 "compilerOptions": {
 "module": "esnext",
 "moduleResolution": "bundler", // 或 "nodenext"
 "target": "es2024">
 }
}

如果您的应用程序仍需 CommonJS 输出以支持较旧的服务器环境,您也可以自行设置 "module": "commonjs"。除非您明确要求,TypeScript 不会再默认使用旧版的输出格式。

更清晰的模块边界:告别 baseUrl 的技巧

过去,团队们常常通过这样的配置来设置路径别名:

// tsconfig.json 中的旧格式
{
 "compilerOptions": {
 "baseUrl": "./",
 "paths": {
 "@components/*": ["src/components/*"],
 "@services/*": ["src/services/*"]
 }
 }
}

早期版本的 TypeScript 在能够解析 paths 映射之前,必须先设置 baseUrl。这一要求带来了不少问题,因为 baseUrl 还允许开发者使用无需相对前缀的导入方式——比如 import { Button } from "src/components/Button"——这使得很难区分项目内的本地文件与从 npm 引入的包。

TypeScript 6 已移除这一依赖:现在 paths 可以独立使用,无需再声明 baseUrl。此外,TypeScript 6 还为 Node.js 的子路径导入提供了原生支持,这种约定使用 # 前缀。

使用原生子路径导入

当前项目无需依赖 TypeScript 特有的别名机制,可直接利用 package.json 中的标准 imports 字段:

// package.json
{
 "name": "my-app",
 "imports": {
 "#services/*": "./src/services/*.js",
 "#utils/*": "./src/utils/*.js"
 }
}

在源文件中,可使用标准的 # 语法来引用这些子路径:

// src/api/user.ts
import { db } from "#services/database";
import { formatName } from "#utils/string";
export function getUser(id: string) {
 const user = db.find(id);
 return formatName(user.name);
}

由于这种机制为 Node.js 自身所理解,且无需在编译端进行重写,因此无论是 TypeScript 6 还是即将推出的 TypeScript 7,文件解析速度都会明显提升。

原样模块语法与仅类型导入

在同时包含运行时代码和类型声明的代码库中,一个常见的问题是仅包含类型的导入语句可能会被误认为是真正的、可执行的导入语句。当你通过普通的 import 语句引入某个接口时,读取该文件的任何工具都必须自行判断该导入语句包含的是实际的 JavaScript 逻辑还是纯粹的编译时类型信息。

TypeScript 6 鼓励团队启用 verbatimModuleSyntax

// tsconfig.json
{
 "compilerOptions": {
 "verbatimModuleSyntax": true
 }
}

启用此选项后,规则就变得非常明确:凡是纯粹用于定义类型的代码,都必须使用 import type 关键字来导入。

// 之前:编译器必须检查 User 和 Order 中是否包含运行时代码
import { User, Order, calculateTotal } from "./billing";
// 之后:对编译器和打包工具而言都更加明确且可预测
import { calculateTotal } from "./billing";
import type { User, Order } from "./billing";

这对 TypeScript 7 为何重要?

即将推出的多核并行编译器会尽可能地独立处理各个文件。当遇到import type语句时,它会立即知道该文件不会生成任何JavaScript输出,因此可以跳过打开并分析./billing.ts的步骤,无需为当前文件确定输出规则。这种小小的优化措施在大型代码库中能显著降低构建开销。

新的内置语言便捷功能

除了默认配置之外,TypeScript 6还带来了一些实用功能,让常见的代码模式更易于编写。

1. Map.getOrInsert()Map.getOrInsertComputed()

试想一下,你有多少次需要编写这类重复的缓存查找逻辑:

// 旧有的、重复性高的写法
const userCache = new Map<string, UserProfile>>();
function getProfile(userId: string): UserProfile {
 let profile = userCache.get(userId);

 if (!profile) {
 profile = fetchProfileFromDatabase(userId);
 userCache.set(userId, profile);
 }

 return profile;
}

TypeScript 6 支持 TC39 提出的 getOrInsertComputed 方法,你可以用它来替代上述写法:

// 现代 TypeScript 6 的写法
const userCache = new Map<string, UserProfile>>();
function getProfile(userId: string): UserProfile {
 // 仅当键不存在时才执行回调函数
 return userCache.getOrInsertComputed(userId, () => {
 return fetchProfileFromDatabase(userId);
 });
}

这种写法更为简洁,无需使用可变的临时变量,并且能始终保持类型推断的准确性。

2. RegExp.escape() 的内置类型

在将特殊字符放入正则表达式之前对其进行净化,过去通常需要自行编写辅助函数或引入第三方库。TypeScript 6 现已为 RegExp.escape() 提供了内置的类型定义:

const userInput = "item.value [test]";
// 安全地转义如 '.'、'[' 和 ']' 这样的字符
const safePattern = RegExp.escape(userInput);
const regex = new RegExp(`^${safePattern}

  
    
    
    
    
    Operator Software for Solar, BESS & Video | REACTAPP.TOP
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    TypeScript 6's Bridge Role in the Path to a Native TS 7 Compiler
    
    
    
    
  
  
    
);

通过这种方式,你无需在项目中添加任何依赖即可避免一大类正则表达式注入漏洞。

为确定性类型排序做准备

TypeScript 6 中一个较为低调但重要的新增功能是 stableTypeOrdering 标志。

在 TypeScript 5.x 及更早版本中,联合类型中各成员的出现顺序取决于编译器在内存中处理文件的顺序。由于编译是在单线程上进行的,因此该顺序在不同运行过程中往往保持一致。

不过,一旦使用像 TypeScript 7 所规划的那样基于并行化的编译器,工作线程每次完成分配的任务顺序都可能不同。如果某个线程比另一个线程先完成任务,结果类型可能会是 string | number;如果重新运行构建过程,或在不同机器上运行,结果则可能是 number | string

为避免生成的 .d.ts 文件出现不可预测的变化,TypeScript 7 在内部采用了严格的确定性排序规则。现在 TypeScript 6 也允许用户选择启用同样的行为:

// tsconfig.json
{
 "compilerOptions": {
 "stableTypeOrdering": true
 }
}

如果你的工作涉及维护开源软件包或将生成的声明文件提交到版本控制系统中,那么现在就开启此选项并进行测试是值得的。这样做可以确保在将来切换到 TypeScript 7 后,你的快照测试和生成的类型不会产生无意义的差异。

实际迁移检查清单

只要分阶段进行,升级大型代码库并不一定很麻烦。请考虑以下优先事项:

首先检查所有与strict相关的标志,这是防止在版本7到来之前出现隐式的any值及未加保护的null引用的最佳手段——优先级很高。 废弃过时的模块配置,停止使用AMD、UMD以及传统的node模块解析策略——优先级很高。 启用verbatimModuleSyntax,这样生成JavaScript时就完全不再依赖类型检查器——优先级中等。 采用带有#前缀的子路径导入方式,从而无需使用针对特定打包工具的路径解析变通方法——优先级中等。 明确设置rootDir,以避免在并行构建时输出文件夹结构出现意外——优先级较低。

应避免的常见错误

错误1:为修复升级错误而禁用严格模式

在升级到 TypeScript 6 后遇到大量新错误时,常见的应对方式就是直接将 "strict": false,这样就能让持续集成再次通过。

这种临时解决方案或许能让你立刻摆脱困境,但实际上只是推迟了真正的问题解决。TypeScript 7 的性能提升是建立在正确类型系统的假设之上的。与其完全关闭严格模式,不如暂时关闭某些具体的检查项——比如 "noImplicitAny": false——然后逐个文件、逐步解决由此产生的错误。

错误 2:混用运行时导入与类型导入

一旦启用了 verbatimModuleSyntax,就不要在同一个语句中同时使用类型导入和值导入:

// 应避免这样做
import { User, UserService } from "./userService";
// 更推荐的做法
import { UserService } from "./userService";
import type { User } from "./userService";

将它们分开可以让读者和编译器都明确知道:哪些导入仅用于类型检查,哪些包含真正的运行时代码。

整体视角:接下来会发生什么?

TypeScript 6 并非要让你学习一堆新的语法。它的真正目的是在更大的变革之前为一切奠定基础。

现在就更新你的 tsconfig.json,改用显式的仅类型导入方式,并摒弃旧式的路径处理方式,这样就能消除日后可能遇到的绝大多数问题。一旦 TypeScript 7 发布,升级操作几乎只需提升包版本并运行新的原生编译器——构建时间将缩短到不到一秒,而且无需重写现有代码。

本周抽点时间检查一下你项目的配置。这虽是小小的投入,但日后会带来巨大的回报。

你的升级计划是什么?

你们的团队已经完全启用严格模式了吗,还是仍在处理旧的配置选项?你们是否已经在自己的项目中尝试过子路径导入?欢迎在评论区分享你的想法和问题。

相关阅读

  • 在 Node 24 中用 Node 的原生测试运行器替代 Jest —— 通过实际迁移案例展示,Node 24 的内置测试运行器及原生 TypeScript 支持如何在减少四个依赖项的同时缩短持续集成时间。
  • 对比 Frontier AI 智能体:Astra、Flash、Fable 和 Mythos —— 详细分析最新的 GPT、Gemini 和 Claude 模型在编码、浏览和工具使用等真实智能体任务中的表现,而不仅仅是基准测试结果。
  • TypeScript 7的Go语言重写:对React类型安全的影响 — 了解TypeScript 7基于Go的编译器如何加快构建速度、优化泛型推断,从而消除React钩子和JSX中的隐藏any类型。
  • TypeScript的Go编译器与原生执行:迁移指南 — 了解基于Go的TypeScript编译器及Node.js原生执行方式将如何影响React和Next.js代码库,以及当前应在tsconfig中做哪些调整。