今天,我们很高兴宣布 TypeScript 5.7 的正式发布!
如果你还不熟悉 TypeScript,它是一种基于 JavaScript 的语言,通过添加类型声明和注解的语法来增强功能。这些语法可以被 TypeScript 编译器用来对代码进行类型检查,并在编译时移除这些类型信息,从而生成简洁、惯用的 JavaScript 代码。类型检查非常有用,因为它可以提前捕获代码中的错误;此外,为代码添加类型还能提升可读性,并让代码编辑器提供强大的功能,比如自动补全、重构、查找引用等。
TypeScript 是 JavaScript 的超集,因此任何有效的 JavaScript 代码也是有效的 TypeScript 代码。事实上,如果你在 Visual Studio 或 VS Code 等编辑器中编写 JavaScript,TypeScript 也在背后为你的 JavaScript 编辑体验提供支持!你可以在 TypeScript 官网 上了解更多信息。
要开始使用 TypeScript,可以通过 npm 运行以下命令:
npm install -D typescript
接下来,让我们看看 TypeScript 5.7 的新功能!
检查从未初始化的变量
长久以来,TypeScript 能够检测出变量在所有前置分支中未被初始化的问题:
let result: number;
if (someCondition()) {
result = doSomeWork();
} else {
let temporaryWork = doSomeWork();
temporaryWork *= 2;
// 忘记将值赋给 'result'
}
console.log(result); // 错误:变量 'result' 在使用前未赋值。
然而,有些情况分析却无法生效。例如,当变量在单独的函数中被访问时,类型系统无法确定函数何时被调用,因此会“乐观”地认为变量已经被初始化:
function foo() {
let result: number;
if (someCondition()) {
result = doSomeWork();
} else {
let temporaryWork = doSomeWork();
temporaryWork *= 2;
// 忘记将值赋给 'result'
}
printResult();
function printResult() {
console.log(result); // 这里没有报错。
}
}
在 TypeScript 5.7 中,尽管对于可能已初始化的变量类型系统仍保持宽松,但它能够对完全未初始化的变量报告错误:
function foo() {
let result: number;
// 执行了一些操作,但忘记赋值给 'result'
function printResult() {
console.log(result); // 错误:变量 'result' 在使用前未赋值。
}
}
这一改进得益于 GitHub 用户 Zzzen 的贡献!
相对路径的路径重写
许多工具和运行时允许你“原地”运行 TypeScript 代码,这意味着不需要一个生成输出 JavaScript 文件的构建步骤。例如,ts-node
、tsx
、Deno
和 Bun
都支持直接运行 .ts
文件。最近,Node.js 也在探索此类支持功能,例如 --experimental-strip-types
(即将去掉实验标志!)和 --experimental-transform-types
。这种功能非常方便,因为它让我们可以更快地迭代,而不必担心反复执行构建任务。
不过,在使用这些模式时有些复杂性需要注意。为了最大程度地兼容所有这些工具,原地运行的 TypeScript 文件在运行时必须以适当的 TypeScript 扩展名进行导入。例如,在 Node.js 的新实验功能中,要导入名为 foo.ts
的文件,我们需要这样写:
// main.ts
import * as foo from "./foo.ts"; // <- 这里需要使用 foo.ts,而不是 foo.js
通常情况下,TypeScript 会对此发出错误,因为它期望我们导入的是输出文件。由于某些工具确实允许 .ts
导入,TypeScript 已经通过 --allowImportingTsExtensions
选项支持这种导入方式。但如果我们需要将这些 .ts
文件生成 .js
文件呢?这是库作者必须面对的要求,他们需要分发 .js
文件。而之前,TypeScript 一直避免重写任何路径。
为了支持这种情况,我们新增了一个编译器选项 --rewriteRelativeImportExtensions
。当导入路径是相对路径(以 ./
或 ../
开头)、以 TypeScript 扩展名(如 .ts
、.tsx
、.mts
、.cts
)结尾,并且是非声明文件时,编译器会将路径重写为对应的 JavaScript 扩展名(如 .js
、.jsx
、.mjs
、.cjs
)。
// 在 --rewriteRelativeImportExtensions 下...
// 这些路径将被重写。
import * as foo from "./foo.ts";
import * as bar from "../someFolder/bar.mts";
// 这些路径不会被重写。
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";
这让我们可以编写既能原地运行又能在需要时编译为 JavaScript 的 TypeScript 代码。
尽管如此,TypeScript 通常避免重写路径,主要原因之一是动态导入。例如,开发者编写以下代码时,很难处理动态导入接收到的路径:
function getPath() {
if (Math.random() < 0.5) {
return "./foo.ts";
} else {
return "./foo.js";
}
}
let myImport = await import(getPath());
另一个问题是(如上所示)只有相对路径会被重写,并且它们的重写是“简单的”。这意味着依赖 TypeScript 的 baseUrl
和 paths
的路径不会被重写:
// tsconfig.json
{
"compilerOptions": {
"module": "nodenext",
// ...
"paths": {
"@/*": ["./src/*"]
}
}
}
// 不会被转换,因此不起作用。
import * as utilities from "@/utilities.ts";
同样,通过 package.json
的 exports
和 imports
字段解析的路径也不会被重写:
// package.json
{
"name": "my-package",
"imports": {
"#root/*": "./dist/*"
}
}
// 不会被转换,因此不起作用。
import * as utilities from "#root/utilities.ts";
因此,如果你在使用多包互相引用的工作区式布局,可能需要使用条件导出和自定义条件来实现:
// 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 在文章 Live types in a 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
的变化,两者现在有所分离。为了弥合差距并保留底层缓冲区类型,所有 TypedArray
(如 Uint8Array
等)现在也变为泛型类型:
interface Uint8Array<TArrayBuffer extends ArrayBufferLike = ArrayBufferLike> {
// ...
}
每个 TypedArray
都包含一个名为 TArrayBuffer
的类型参数,不过该参数具有默认值,因此仍可以直接使用 Int32Array
,而无需显式写出 Int32Array<ArrayBufferLike>
。
如果在更新后遇到任何问题,可能需要更新 @types/node
。
这一工作主要得益于 Kenta Moriuchi 的贡献!
搜索Root配置文件以确定项目归属
当使用 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
是用于运行测试的配置文件。
// src/tsconfig.json
{
"compilerOptions": {
"outDir": "../dist"
},
"exclude": ["**/*.test.ts"]
}
// src/tsconfig.test.json
{
"compilerOptions": {
"outDir": "../dist/test"
},
"include": ["**/*.test.ts"],
"references": [
{ "path": "./tsconfig.json" }
]
}
// tsconfig.json
{
"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
目录是主项目,依赖所有其他子项目。
// app/tsconfig.json
{
"compilerOptions": {
// ...
},
"include": ["src"],
"references": [
{ "path": "../graphics/tsconfig.json" },
{ "path": "../sound/tsconfig.json" },
{ "path": "../networking/tsconfig.json" },
{ "path": "../input/tsconfig.json" }
]
}
注意,这里有一个文件 some-script.js
位于 app
目录。当在编辑器中打开 some-script.js
时,TypeScript 语言服务需要确定该文件属于哪个项目,以应用正确的设置。
此前的行为:
在这种情况下,最近的 tsconfig.json
文件不包含 some-script.js
。因此,TypeScript 会尝试检查 app/tsconfig.json
所引用的每个项目,看是否包含 some-script.js
。过去的做法是逐一加载每个项目,并在找到包含该文件的项目时停止。然而,即使 some-script.js
不在根文件集中,TypeScript 仍会解析项目中的所有文件,因为根文件集中的一些文件可能会通过依赖关系间接引用 some-script.js
。
这种行为在大型代码库中可能导致极端且不可预测的性能问题。开发者打开某些独立脚本文件时,可能会发现整个代码库被加载,严重影响效率。
TypeScript 5.7 的改进:
每个可以被其他项目引用的项目(非工作区项目)都必须启用 composite
标志,该标志要求所有输入源文件必须提前声明。因此,在检查复合项目时,TypeScript 5.7 只会验证文件是否属于项目的根文件集,从而避免最糟糕的情况。
有关此更改的更多信息,请参见此处。
在 --module nodenext
中验证 JSON 导入
在使用 --module nodenext
时,从 .json
文件导入将需要遵循特定规则,以防止运行时错误。
规则 1:必须提供 type: "json"
的导入属性
当导入 JSON 文件时,必须包含带有 type: "json"
的导入属性,否则会报错:
import myConfig from "./myConfig.json";
// ~~~~~~~~~~~~~~~~~
// ❌ 错误:在 ECMAScript 模块中导入 JSON 文件时,如果 'module' 设置为 'NodeNext',需要使用 'type: "json"' 的导入属性。
import myConfig from "./myConfig.json" with { type: "json" };
// ^^^^^^^^^^^^^^^^
// ✅ 正确:因为我们提供了 `type: "json"`
规则 2:不支持“命名”导出
导入 JSON 文件的内容只能通过默认导入访问,TypeScript 不会生成命名导出。
// ✅ 这是正确的方式:
import myConfigA from "./myConfig.json" with { type: "json" };
let version = myConfigA.version;
///////////
import * as myConfigB from "./myConfig.json" with { type: "json" };
// ❌ 错误:不能直接访问命名导出
let version = myConfig.version;
// ✅ 正确:通过默认导入访问
let version = myConfig.default.version;
更多细节可以参见此处。
支持 Node.js 中的 V8 编译缓存
Node.js 22 新增了 module.enableCompileCache()
API,允许运行时重用工具在首次运行后的一部分解析和编译工作。
TypeScript 5.7 开始利用该 API,以提高工具的启动速度。在测试中,运行 tsc --version
的速度提高了约 2.5 倍:
Benchmark 1: node ./built/local/_tsc.js --version (*未启用* 缓存)
平均时间 (mean ± σ): 122.2 ms ± 1.5 ms [用户时间: 101.7 ms, 系统时间: 13.0 ms]
范围 (min … max): 119.3 ms … 132.3 ms 200 次运行
Benchmark 2: node ./built/local/tsc.js --version (*启用* 缓存)
平均时间 (mean ± σ): 48.4 ms ± 1.0 ms [用户时间: 34.0 ms, 系统时间: 11.1 ms]
范围 (min … max): 45.7 ms … 52.8 ms 200 次运行
总结:
启用缓存后运行速度
比未启用缓存快了 2.52 ± 0.06 倍
更多信息可以参见此处。
重要变更
以下列出了一些值得注意的变更,这些变更可能会影响现有的代码库:
1. lib.d.ts
的更新
针对 DOM 的类型定义更新可能会影响代码库的类型检查。更多详情请参阅与 DOM 和 lib.d.ts
更新相关的 issues。
2. TypedArray 现在支持泛型
在 ECMAScript 2024 中,SharedArrayBuffer
和 ArrayBuffer
的类型定义有所分歧。为了弥合这些差异并保留底层缓冲区类型,所有 TypedArray(如 Uint8Array
等)现在都支持泛型:
interface 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 现在对类中用非字面量计算属性名声明的方法的行为更加一致。例如,以下代码:
declare const symbolMethodName: symbol;
export class A {
[symbolMethodName]() { return 1; }
}
在 TypeScript 以前的版本中,上述代码中的 [symbolMethodName]
方法不会对 A
的类型系统产生任何影响,等效于:
export class A {
}
换句话说,从类型系统的角度来看,[symbolMethodName]
对 A
的类型没有任何贡献。
在 TypeScript 5.7 中,方法 [symbolMethodName]()
会被更有意义地处理,并生成索引签名。因此,上述代码会被解释为:
export class A {
[x: symbol]: () => number;
}
这使得它的行为与对象字面量中的属性和方法保持一致。
更多关于此更改的信息,请查看这里。
对返回 null
和 undefined
的函数的更多隐式 any
错误
当函数表达式在上下文中由返回泛型类型的签名类型化时,TypeScript 现在会在 noImplicitAny
开启但 strictNullChecks
关闭的情况下,正确提供隐式 any
的错误。
示例
以下代码中,catch
的回调返回 null
:
declare var p: Promise<number>;
const p2 = p.catch(() => null);
// ~~~~~~~~~~
// error TS7011: Function expression, which lacks return-type annotation, implicitly has an 'any' return type.
在这种情况下,TypeScript 会抛出错误,提示缺少返回类型注解。
更多详情请参见此更改。
下一步是什么?
很快,我们将分享下一版本 TypeScript 的计划细节。如果您想提前体验最新的修复和功能,可以通过以下方式获取:
- 在 npm 上使用 TypeScript 的 nightly 构建版本。
- 安装适用于 Visual Studio Code 的扩展,以使用这些 nightly 版本。
我们希望 TypeScript 5.7 能让您的编码过程更加愉快。Happy Hacking!
—— Daniel Rosenwasser 和 TypeScript 团队