检查从未初始化的变量
长期以来,TypeScript 一直能够捕获变量在所有先前的分支中尚未初始化的相关问题。
tslet result: numberif (someCondition()) {result = doSomeWork();}else {let temporaryWork = doSomeWork();temporaryWork *= 2;// forgot to assign to 'result'}console.log(result); // error: Variable 'result' is used before being assigned.
遗憾的是,在某些情况下此分析并不起作用。例如,如果变量是在一个单独的函数中被访问的,类型系统就无法知道该函数何时会被调用,因此会采取一种“乐观”的观点,认为该变量已被初始化。
tsfunction foo() {let result: numberif (someCondition()) {result = doSomeWork();}else {let temporaryWork = doSomeWork();temporaryWork *= 2;// forgot to assign to 'result'}printResult();function printResult() {console.log(result); // no error here.}}
虽然 TypeScript 5.7 对于那些可能已初始化的变量仍然保持宽容,但类型系统现在能够在变量从未初始化的情况下报告错误。
tsfunction foo() {let result: number// do work, but forget to assign to 'result'function printResult() {console.log(result); // error: Variable 'result' is used before being assigned.}}
相对路径的路径重写
有多种工具和运行时允许你“就地”(in-place)运行 TypeScript 代码,这意味着它们不需要生成输出 JavaScript 文件的构建步骤。例如,ts-node、tsx、Deno 和 Bun 都支持直接运行 .ts 文件。最近,Node.js 也在探索通过 --experimental-strip-types(很快将移除标志!)和 --experimental-transform-types 来实现此类支持。这非常方便,因为它让我们能够更快地迭代,而无需担心重新运行构建任务。
不过,在使用这些模式时需要注意一些复杂性。为了与所有这些工具实现最大程度的兼容,在运行时“就地”导入的 TypeScript 文件必须使用相应的 TypeScript 扩展名导入。例如,要导入一个名为 foo.ts 的文件,在使用 Node 的新实验性支持时,我们必须这样写:
ts// main.tsimport * as foo from "./foo.ts"; // <- we need foo.ts here, not foo.js
通常情况下,如果我们这样做,TypeScript 会报错,因为它期望我们导入的是输出文件。由于一些工具确实允许导入 .ts,TypeScript 此前已通过一个名为 --allowImportingTsExtensions 的选项支持了这种导入风格。这工作得很好,但如果我们需要从这些 .ts 文件中实际生成 .js 文件呢?这是那些需要分发纯 .js 文件的库作者的需求,但到目前为止,TypeScript 一直避免重写任何路径。
为了支持这种情况,我们添加了一个新的编译器选项 --rewriteRelativeImportExtensions。当导入路径为相对路径(以 ./ 或 ../ 开头)、以 TypeScript 扩展名结尾(.ts, .tsx, .mts, .cts)且为非声明文件时,编译器会将路径重写为相应的 JavaScript 扩展名(.js, .jsx, .mjs, .cjs)。
ts// Under --rewriteRelativeImportExtensions...// these will be rewritten.import * as foo from "./foo.ts";import * as bar from "../someFolder/bar.mts";// these will NOT be rewritten in any way.import * as a from "./foo";import * as b from "some-package/file.ts";import * as c from "@some-scope/some-package/file.ts";import * as d from "#/file.ts";import * as e from "./file.js";
这使我们能够编写可以就地运行的 TypeScript 代码,并在准备好后编译成 JavaScript。
我们注意到,TypeScript 通常避免重写路径。原因有几个,但最明显的一个是动态导入。如果开发者写了如下代码,处理 import 接收到的路径并非易事。事实上,在任何依赖项中覆盖 import 的行为都是不可能的。
tsfunction getPath() {if (Math.random() < 0.5) {return "./foo.ts";}else {return "./foo.js";}}let myImport = await import(getPath());
另一个问题是(如上所述)只有相对路径会被重写,而且它们是“简单地”重写的。这意味着任何依赖于 TypeScript 的 baseUrl 和 paths 的路径都不会被重写。
json// tsconfig.json{"compilerOptions": {"module": "nodenext",// ..."paths": {"@/*": ["./src/*"]}}}
ts// Won't be transformed, won't work.import * as utilities from "@/utilities.ts";
任何可能通过 package.json 的 exports 和 imports 字段解析的路径也不会被重写。
json// package.json{"name": "my-package","imports": {"#root/*": "./dist/*"}}
ts// Won't be transformed, won't work.import * as utilities from "#root/utilities.ts";
因此,如果你一直使用多个包相互引用的工作区风格布局,你可能需要使用带作用域自定义条件的条件导出来使此功能生效。
json// my-package/package.json{"name": "my-package","exports": {".": {"@my-package/development": "./src/index.ts","import": "./lib/index.js"},"./*": {"@my-package/development": "./src/*.ts","import": "./lib/*.js"}}}
每当你想导入 .ts 文件时,你可以使用 node --conditions=@my-package/development 运行它。
请注意我们为 @my-package/development 条件使用的“命名空间”或“作用域”。这是一种权宜之计,旨在避免可能同样使用 development 条件的依赖项产生冲突。如果每个人都在他们的包中发布了一个 development 条件,那么解析过程可能会尝试解析为一个 .ts 文件,而这未必可行。这个想法类似于 Colin McDonnell 在其文章TypeScript monorepo 中的实时类型中所描述的,以及 tshy 加载源码的指南。
有关此功能工作原理的更多详情,请参阅此处的更改。
支持 --target es2024 和 --lib es2024
TypeScript 5.7 现在支持 --target es2024,允许用户针对 ECMAScript 2024 运行时进行开发。此目标主要用于指定新的 --lib es2024,其中包含 SharedArrayBuffer 和 ArrayBuffer、Object.groupBy、Map.groupBy、Promise.withResolvers 等的多项特性。它还将 Atomics.waitAsync 从 --lib es2022 移至 --lib es2024。
请注意,作为 SharedArrayBuffer 和 ArrayBuffer 更改的一部分,两者现在有所分歧。为了弥合这一差距并保留底层缓冲区类型,所有 TypedArrays(如 Uint8Array 等)现在也变成了泛型。
tsinterface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> {// ...}
每个 TypedArray 现在都包含一个名为 TArrayBuffer 的类型参数,不过该类型参数具有默认类型实参,以便我们可以继续引用 Int32Array,而无需显式写出 Int32Array<ArrayBufferLike>。
如果在此更新中遇到任何问题,你可能需要更新 @types/node。
这项工作主要由 Kenta Moriuchi 提供!
搜索上级配置文件以确定项目所有权
当使用 TSServer(如 Visual Studio 或 VS Code)在编辑器中加载 TypeScript 文件时,编辑器会尝试找到“拥有”该文件的相关 tsconfig.json 文件。为此,它会从所编辑的文件开始沿目录树向上查找名为 tsconfig.json 的文件。
以前,此搜索会在找到的第一个 tsconfig.json 文件处停止;但试想如下项目结构:
project/├── src/│ ├── foo.ts│ ├── foo-test.ts│ ├── tsconfig.json│ └── tsconfig.test.json└── tsconfig.json
在这里,src/tsconfig.json 是项目的“主要”配置文件,而 src/tsconfig.test.json 是用于运行测试的配置文件。
json// src/tsconfig.json{"compilerOptions": {"outDir": "../dist"},"exclude": ["**/*.test.ts"]}
json// src/tsconfig.test.json{"compilerOptions": {"outDir": "../dist/test"},"include": ["**/*.test.ts"],"references": [{ "path": "./tsconfig.json" }]}
json// tsconfig.json{// This is a "workspace-style" or "solution-style" tsconfig.// Instead of specifying any files, it just references all the actual projects."files": [],"references": [{ "path": "./src/tsconfig.json" },{ "path": "./src/tsconfig.test.json" },]}
问题在于,当编辑 foo-test.ts 时,编辑器会将 project/src/tsconfig.json 视为“所属”配置文件——但这并不是我们想要的!如果查找在此处停止,可能并不理想。此前避免这种情况的唯一方法是将 src/tsconfig.json 重命名为 src/tsconfig.src.json 之类,然后所有文件都会匹配到引用每个可能项目的顶层 tsconfig.json。
project/├── src/│ ├── foo.ts│ ├── foo-test.ts│ ├── tsconfig.src.json│ └── tsconfig.test.json└── tsconfig.json
为了不再强制开发者这样做,TypeScript 5.7 现在会继续沿目录树向上查找,以寻找其他合适的 tsconfig.json 文件以供编辑器场景使用。这可以为项目的组织和配置文件的结构提供更多灵活性。
在编辑器中对复合项目进行更快的项目所有权检查
想象一个具有以下结构的大型代码库:
packages├── graphics/│ ├── tsconfig.json│ └── src/│ └── ...├── sound/│ ├── tsconfig.json│ └── src/│ └── ...├── networking/│ ├── tsconfig.json│ └── src/│ └── ...├── input/│ ├── tsconfig.json│ └── src/│ └── ...└── app/├── tsconfig.json├── some-script.js└── src/└── ...
packages 中的每个目录都是一个独立的 TypeScript 项目,而 app 目录是依赖于所有其他项目的主项目。
json// app/tsconfig.json{"compilerOptions": {// ...},"include": ["src"],"references": [{ "path": "../graphics/tsconfig.json" },{ "path": "../sound/tsconfig.json" },{ "path": "../networking/tsconfig.json" },{ "path": "../input/tsconfig.json" }]}
现在注意,我们在 app 目录中有一个 some-script.js 文件。当我们在编辑器中打开 some-script.js 时,TypeScript 语言服务(它也处理 JavaScript 文件的编辑器体验!)必须找出该文件属于哪个项目,以便应用正确的设置。
在这种情况下,最近的 tsconfig.json 并不包含 some-script.js,但 TypeScript 会继续询问“app/tsconfig.json 引用的项目中是否有包含 some-script.js 的项目?”。为了做到这一点,TypeScript 以前会逐个加载每个项目,并在找到包含 some-script.js 的项目后立即停止。即使 some-script.js 未包含在根文件集中,TypeScript 仍会解析项目内的所有文件,因为部分根文件集可能仍然间接引用了 some-script.js。
我们发现,这种行为在大型代码库中会导致极其不稳定和不可预测的表现。开发者在打开杂乱的脚本文件时,会发现自己不得不等待整个代码库被加载。
值得庆幸的是,任何可以被另一个(非工作区)项目引用的项目都必须启用 composite 标志,该标志强制执行所有输入源文件必须预先已知的规则。因此,在探测 composite 项目时,TypeScript 5.7 将只检查文件是否属于该项目的根文件集。这应该能避免这种常见的极端情况。
更多信息,请参阅此处的更改。
在 --module nodenext 中验证 JSON 导入
在 --module nodenext 下从 .json 文件导入时,TypeScript 现在将强制执行某些规则以防止运行时错误。
首先,对于任何 JSON 文件导入,必须存在包含 type: "json" 的导入属性。
tsimport myConfig from "./myConfig.json";// ~~~~~~~~~~~~~~~~~// ❌ error: Importing a JSON file into an ECMAScript module requires a 'type: "json"' import attribute when 'module' is set to 'NodeNext'.import myConfig from "./myConfig.json" with { type: "json" };// ^^^^^^^^^^^^^^^^// ✅ This is fine because we provided `type: "json"`
在此验证之上,TypeScript 将不会生成“命名”导出,JSON 导入的内容只能通过默认导出(default)访问。
ts// ✅ This is okay:import myConfigA from "./myConfig.json" with { type: "json" };let version = myConfigA.version;///////////import * as myConfigB from "./myConfig.json" with { type: "json" };// ❌ This is not:let version = myConfig.version;// ✅ This is okay:let version = myConfig.default.version;
点击此处了解更多有关此更改的信息。
支持 Node.js 中的 V8 编译缓存
Node.js 22 支持一项名为 module.enableCompileCache() 的新 API。此 API 允许运行时重用工具首次运行后完成的部分解析和编译工作。
TypeScript 5.7 现在利用了该 API,以便更快地开始执行有意义的工作。在我们自己的某些测试中,我们观察到运行 tsc --version 的速度提升了约 2.5 倍。
Benchmark 1: node ./built/local/_tsc.js --version (*without* caching)Time (mean ± σ): 122.2 ms ± 1.5 ms [User: 101.7 ms, System: 13.0 ms]Range (min … max): 119.3 ms … 132.3 ms 200 runsBenchmark 2: node ./built/local/tsc.js --version (*with* caching)Time (mean ± σ): 48.4 ms ± 1.0 ms [User: 34.0 ms, System: 11.1 ms]Range (min … max): 45.7 ms … 52.8 ms 200 runsSummarynode ./built/local/tsc.js --version ran2.52 ± 0.06 times faster than node ./built/local/_tsc.js --version
更多信息,请参阅此处的 Pull Request。
显著的行为更改
本节重点介绍了一组值得注意的更改,在进行任何升级时都应予以确认和理解。有时它会突出显示弃用、移除和新的限制。它也可能包含功能上有所改进但通过引入新错误而可能影响现有构建的错误修复。
lib.d.ts
为 DOM 生成的类型可能会对代码库的类型检查产生影响。更多信息,请查看与此版本 TypeScript 的 DOM 和 lib.d.ts 更新相关的问题。
TypedArray 现在是 ArrayBufferLike 的泛型
在 ECMAScript 2024 中,SharedArrayBuffer 和 ArrayBuffer 的类型略有不同。为了弥合这一差距并保留底层缓冲区类型,所有 TypedArrays(如 Uint8Array 等)现在也变成了泛型。
tsinterface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> {// ...}
每个 TypedArray 现在都包含一个名为 TArrayBuffer 的类型参数,不过该类型参数具有默认类型实参,以便用户可以继续引用 Int32Array,而无需显式写出 Int32Array<ArrayBufferLike>。
如果在此更新中遇到任何问题,例如:
error TS2322: Type 'Buffer' is not assignable to type 'Uint8Array<ArrayBufferLike>'.error TS2345: Argument of type 'Buffer' is not assignable to parameter of type 'Uint8Array<ArrayBufferLike>'.error TS2345: Argument of type 'ArrayBufferLike' is not assignable to parameter of type 'ArrayBuffer'.error TS2345: Argument of type 'Buffer' is not assignable to parameter of type 'string | ArrayBufferView | Stream | Iterable<string | ArrayBufferView> | AsyncIterable<string | ArrayBufferView>'.
那么你可能需要更新 @types/node。
从类中的非字面量方法名创建索引签名
TypeScript 现在对类中以非字面量计算属性名声明的方法有了更一致的行为。例如,在以下代码中:
tsdeclare const symbolMethodName: symbol;export class A {[symbolMethodName]() { return 1 };}
以前,TypeScript 仅以如下方式看待类:
tsexport class A {}
换句话说,从类型系统的角度来看,[symbolMethodName] 对 A 的类型没有任何贡献。
TypeScript 5.7 现在更具意义地看待方法 [symbolMethodName]() {},并生成了一个索引签名。因此,上述代码被解释为类似于以下代码:
tsexport class A {[x: symbol]: () => number;}
这提供了与对象字面量中的属性和方法一致的行为。
对返回 null 和 undefined 的函数产生更多的隐式 any 错误
当函数表达式被返回泛型类型的签名进行上下文类型化时,TypeScript 现在会在 noImplicitAny 下、但在 strictNullChecks 之外适当地提供隐式 any 错误。
tsdeclare var p: Promise<number>;const p2 = p.catch(() => null);// ~~~~~~~~~~// error TS7011: Function expression, which lacks return-type annotation, implicitly has an 'any' return type.