TypeScript 5.9
精简与更新的 tsc --init
一段时间以来,TypeScript 编译器支持 --init 标志,可以在当前目录中创建 tsconfig.json。 在过去的几年里,运行 tsc --init 会创建一个非常“完整”的 tsconfig.json,其中充满了注释掉的设置及其描述。 我们这样设计的意图是使选项易于发现和切换。
然而,根据外部反馈(和我们自己的经验),我们发现用户通常会立即删除这些新 tsconfig.json 文件的大部分内容。 当用户想要发现新选项时,我们发现他们依赖于编辑器的自动补全,或者导航到我们网站上的 tsconfig 参考(生成的 tsconfig.json 会链接到那里!)。 每个设置的作用也记录在同一页面上,并可以通过编辑器的悬停/工具提示/快速信息看到。 虽然展示一些注释掉的设置可能有所帮助,但生成的 tsconfig.json 通常被认为过于冗余。
我们还认为,是时候让 tsc --init 初始化一些比我们已经启用的更具规范性的设置了。 我们查看了用户在创建新 TypeScript 项目时遇到的一些常见痛点和细节问题。 例如,大多数用户使用模块编写(而不是全局脚本),而 --moduleDetection 可以强制 TypeScript 将每个实现文件视为模块。 开发人员还经常希望直接在其运行时中使用最新的 ECMAScript 特性,因此 --target 通常可以设置为 esnext。 JSX 用户经常发现回头设置 --jsx 是不必要的摩擦,而且其选项有些令人困惑。 此外,项目最终加载的 node_modules/@types 中的声明文件通常比 TypeScript 实际需要的要多;但指定一个空的 types 数组可以帮助限制这一点。
在 TypeScript 5.9 中,不带其他标志的普通 tsc --init 将生成以下 tsconfig.json:
{
// 访问 https://aka.ms/tsconfig 阅读有关此文件的更多信息
"compilerOptions": {
// 文件布局
// "rootDir": "./src",
// "outDir": "./dist",
// 环境设置
// 另请参阅 https://aka.ms/tsconfig_modules
"module": "nodenext",
"target": "esnext",
"types": [],
// 对于 nodejs:
// "lib": ["esnext"],
// "types": ["node"],
// 并运行 npm install -D @types/node
// 其他输出
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// 更严格的类型检查选项
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// 风格选项
// "noImplicitReturns": true,
// "noImplicitOverride": true,
// "noUnusedLocals": true,
// "noUnusedParameters": true,
// "noFallthroughCasesInSwitch": true,
// "noPropertyAccessFromIndexSignature": true,
// 推荐选项
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true,
}
}支持 import defer
TypeScript 5.9 引入了对 ECMAScript 延迟模块求值提案 的支持,使用新的 import defer 语法。 此功能允许你导入模块而无需立即执行该模块及其依赖项,从而更好地控制工作和副作用发生的时间。
该语法仅允许命名空间导入:
import defer * as feature from "./some-feature.js";import defer 的主要好处是模块仅在首次访问其导出之一时才被求值。 考虑以下示例:
// ./some-feature.ts
initializationWithSideEffects();
function initializationWithSideEffects() {
// ...
specialConstant = 42;
console.log("Side effects have occurred!");
}
export let specialConstant: number;当使用 import defer 时,initializationWithSideEffects() 函数将不会被调用,直到你实际访问导入命名空间的属性:
import defer * as feature from "./some-feature.js";
// 尚未发生副作用
// ...
// 一旦访问 `specialConstant`,`feature` 模块的内容
// 就会运行,副作用也发生了。
console.log(feature.specialConstant); // 42因为模块的求值被延迟到访问模块的成员时,你不能使用命名导入或默认导入与 import defer:
// ❌ 不允许
import defer { doSomething } from "some-module";
// ❌ 不允许
import defer defaultExport from "some-module";
// ✅ 仅支持此语法
import defer * as feature from "some-module";请注意,当你编写 import defer 时,模块及其依赖项已完全加载并准备好执行。 这意味着模块必须存在,并将从文件系统或网络资源加载。 普通 import 和 import defer 之间的关键区别在于语句和声明的执行被延迟,直到你访问导入命名空间的属性。
此功能对于有条件地加载具有昂贵或平台特定初始化的模块特别有用。它还可以通过将应用功能的模块求值延迟到实际需要时来提高启动性能。
请注意,TypeScript 根本不会转换或“降级” import defer。 它旨在用于原生支持该功能的运行时,或由可以应用适当转换的工具(如打包器)使用。 这意味着 import defer 仅在 --module 模式 preserve 和 esnext 下工作。
我们要感谢 Nicolò Ribaudo 在 TC39 中推动该提案,并为此功能提供了实现。
支持 --module node20
TypeScript 为 --module 和 --moduleResolution 设置提供了多个 node* 选项。 最近,--module nodenext 支持从 CommonJS 模块 require() ECMAScript 模块,并正确拒绝导入断言(支持标准化的导入属性)。
TypeScript 5.9 为这些设置带来了一个稳定的选项,称为 node20,旨在模拟 Node.js v20 的行为。 与 --module nodenext 或 --moduleResolution nodenext 不同,此选项将来不太可能有新行为。 同样与 nodenext 不同的是,除非另行配置,否则指定 --module node20 将隐含 --target es2023。 另一方面,--module nodenext 隐含浮动的 --target esnext。
有关更多信息,请查看此处的实现。
DOM API 中的摘要描述
以前,TypeScript 中的许多 DOM API 仅链接到该 API 的 MDN 文档。 这些链接很有用,但没有提供 API 功能的快速摘要。 感谢 Adam Naji 的一些更改,TypeScript 现在包含了许多基于 MDN 文档的 DOM API 的摘要描述。 你可以在这里和这里看到更多这些更改。
可展开的悬停提示(预览)
快速信息(也称为“编辑器工具提示”和“悬停提示”)对于查看变量以了解其类型,或查看类型别名以了解其实际指向的内容非常有用。 尽管如此,人们常常想要更深入,从快速信息工具提示中显示的任何内容获取更多细节。 例如,如果我们在以下示例中将鼠标悬停在参数 options 上:
export function drawButton(options: Options): void我们只看到 (parameter) options: Options。

我们真的需要跳转到类型 Options 的定义才能看到该值有哪些成员吗?
以前,确实是这样的。 为了帮助解决这个问题,TypeScript 5.9 现在预览了一个称为可展开悬停提示(或“快速信息详细程度”)的功能。 如果你使用像 VS Code 这样的编辑器,你现在会在这些悬停工具提示的左侧看到一个 + 和 - 按钮。 单击 + 按钮将更深入地展开类型,而单击 - 按钮将折叠到上一个视图。
此功能目前处于预览阶段,我们正在为 TypeScript 和我们在 Visual Studio Code 上的合作伙伴寻求反馈。 有关更多详细信息,请参阅此功能的 PR。
可配置的最大悬停提示长度
有时,快速信息工具提示会变得非常长,以至于 TypeScript 会截断它们以使其更具可读性。 这里的缺点是,通常最重要的信息会从悬停工具提示中省略,这可能令人沮丧。 为了解决这个问题,TypeScript 5.9 的语言服务器支持可配置的悬停提示长度,可以通过 VS Code 中的 js/ts.hover.maximumLength 设置进行配置。
此外,新的默认悬停提示长度比以前的默认值大得多。 这意味着在 TypeScript 5.9 中,默认情况下你应该在悬停工具提示中看到更多信息。 有关更多详细信息,请参阅此功能的 PR 和对 Visual Studio Code 的相应更改。
优化
映射器上的实例化缓存
当 TypeScript 用特定类型参数替换类型参数时,最终可能会重复实例化许多相同的中间类型。 在像 Zod 和 tRPC 这样的复杂库中,这可能导致性能问题以及关于类型实例化深度过大的错误报告。 感谢 Mateusz Burzyński 的一项更改,TypeScript 5.9 能够在特定类型实例化已经开始工作时缓存许多中间实例化。 这反过来避免了大量不必要的工作和分配。
避免在 fileOrDirectoryExistsUsingSource 中创建闭包
在 JavaScript 中,函数表达式通常会分配一个新的函数对象,即使包装函数只是将参数传递给另一个没有捕获变量的函数。 在文件存在性检查的代码路径中,Vincent Bailly 发现了这些传递函数调用的例子,尽管底层函数只接受单个参数。 考虑到较大项目中可能进行的存在性检查的数量,他报告了大约 11% 的速度提升。 在此处查看更多关于此更改的信息。
值得注意的行为变化
lib.d.ts 更改
为 DOM 生成的类型可能会对代码库的类型检查产生影响。
此外,一个值得注意的变化是 ArrayBuffer 已经更改,使其不再是几个不同 TypedArray 类型的超类型。 这还包括 UInt8Array 的子类型,例如 Node.js 的 Buffer。 因此,你会看到新的错误消息,例如:
error TS2345: Argument of type 'ArrayBufferLike' is not assignable to parameter of type 'BufferSource'.
error TS2322: Type 'ArrayBufferLike' is not assignable to type 'ArrayBuffer'.
error TS2322: Type 'Buffer' is not assignable to type 'Uint8Array<ArrayBufferLike>'.
error TS2322: Type 'Buffer' is not assignable to type 'ArrayBuffer'.
error TS2345: Argument of type 'Buffer' is not assignable to parameter of type 'string | Uint8Array<ArrayBufferLike>'.如果你遇到 Buffer 的问题,你可能首先想检查你是否使用的是最新版本的 @types/node 包。 这可能包括运行
npm update @types/node --save-dev很多时候,解决方案是指定更具体的底层缓冲区类型,而不是使用默认的 ArrayBufferLike(即显式写出 Uint8Array<ArrayBuffer> 而不是普通的 Uint8Array)。 在某些情况下,当某个 TypedArray(如 Uint8Array)被传递给期望 ArrayBuffer 或 SharedArrayBuffer 的函数时,你也可以尝试访问该 TypedArray 的 buffer 属性,如下例所示:
let data = new Uint8Array([0, 1, 2, 3, 4]);
- someFunc(data)
+ someFunc(data.buffer)类型参数推断更改
为了解决推断过程中类型变量的“泄漏”问题,TypeScript 5.9 可能会在某些代码库中引入类型更改,甚至可能引入新的错误。 这些难以预测,但通常可以通过向泛型函数调用添加类型参数来修复。 在此处查看更多详细信息。