TypeScript 5.5

推断类型谓词

🌐 Inferred Type Predicates

本节由 Dan Vanderkam 撰写,他 在 TypeScript 5.5 中实现了此功能。感谢 Dan!

TypeScript 的控制流分析在跟踪变量类型在代码中移动时的变化方面做得非常出色:

🌐 TypeScript’s control flow analysis does a great job of tracking how the type of a variable changes as it moves through your code:

tsx
interface Bird {
commonName: string;
scientificName: string;
sing(): void;
}
// Maps country names -> national bird.
// Not all nations have official birds (looking at you, Canada!)
declare const nationalBirds: Map<string, Bird>;
function makeNationalBirdCall(country: string) {
const bird = nationalBirds.get(country); // bird has a declared type of Bird | undefined
if (bird) {
bird.sing(); // bird has type Bird inside the if statement
} else {
// bird has type undefined here.
}
}

通过让你处理 undefined 情况,TypeScript 促使你编写更健壮的代码。

🌐 By making you handle the undefined case, TypeScript pushes you to write more robust code.

在过去,这种类型精炼更难应用于数组。在所有以前版本的 TypeScript 中,这都会是一个错误:

🌐 In the past, this sort of type refinement was more difficult to apply to arrays. This would have been an error in all previous versions of TypeScript:

tsx
function makeBirdCalls(countries: string[]) {
// birds: (Bird | undefined)[]
const birds = countries
.map(country => nationalBirds.get(country))
.filter(bird => bird !== undefined);
for (const bird of birds) {
bird.sing(); // error: 'bird' is possibly 'undefined'.
}
}

这段代码完全没问题:我们已经把列表中的所有 undefined 值过滤掉了。但 TypeScript 还没能跟上。

🌐 This code is perfectly fine: we’ve filtered all the undefined values out of the list. But TypeScript hasn’t been able to follow along.

在 TypeScript 5.5 中,类型检查器可以处理以下代码:

🌐 With TypeScript 5.5, the type checker is fine with this code:

tsx
function makeBirdCalls(countries: string[]) {
// birds: Bird[]
const birds = countries
.map(country => nationalBirds.get(country))
.filter(bird => bird !== undefined);
for (const bird of birds) {
bird.sing(); // ok!
}
}

注意 birds 的更精确类型。

🌐 Note the more precise type for birds.

这是可行的,因为 TypeScript 现在会为 filter 函数推断一个 类型谓词。 通过将其提取到一个独立的函数中,你可以更清楚地看到发生了什么:

🌐 This works because TypeScript now infers a type predicate for the filter function. You can see what’s going on more clearly by pulling it out into a standalone function:

tsx
// function isBirdReal(bird: Bird | undefined): bird is Bird
function isBirdReal(bird: Bird | undefined) {
return bird !== undefined;
}

bird is Bird 是类型谓词。
这意味着,如果函数返回 true,那么它是 Bird(如果函数返回 false,那么它是 undefined)。
Array.prototype.filter 的类型声明了解类型谓词,因此最终结果是你获得更精确的类型,并且代码能够通过类型检查。

如果满足以下条件,TypeScript 会推断函数返回类型谓词:

🌐 TypeScript will infer that a function returns a type predicate if these conditions hold:

  1. 该函数没有显式返回类型或类型谓词注解。
  2. 该函数只有一个 return 语句,并且没有隐式返回。
  3. 该函数不会改变其参数。
  4. 该函数返回一个与参数上的精炼绑定的 boolean 表达式。

一般来说,这个工作方式和你预期的一样。这里有一些推断类型谓词的更多例子:

🌐 Generally this works how you’d expect. Here’s a few more examples of inferred type predicates:

tsx
// const isNumber: (x: unknown) => x is number
const isNumber = (x: unknown) => typeof x === 'number';
// const isNonNullish: <T>(x: T) => x is NonNullable<T>
const isNonNullish = <T,>(x: T) => x != null;

以前,TypeScript 只是推断这些函数返回 boolean。 现在,它会推断带有类型谓词的签名,比如 x is numberx is NonNullable<T>

🌐 Previously, TypeScript would have just inferred that these functions return boolean. It now infers signatures with type predicates like x is number or x is NonNullable<T>.

类型谓词具有“当且仅当”的语义。 如果一个函数返回 x is T,那么这意味着:

🌐 Type predicates have “if and only if” semantics. If a function returns x is T, then it means that:

  1. 如果函数返回 true,那么 x 的类型是 T
  2. 如果函数返回 false,那么 x 不是 T 类型。

如果你期望类型谓词被推断出来,但没有,那么你可能违反了第二条规则。这通常出现在“真实性”检查中:

🌐 If you’re expecting a type predicate to be inferred but it’s not, then you may be running afoul of the second rule. This often comes up with “truthiness” checks:

tsx
function getClassroomAverage(students: string[], allScores: Map<string, number>) {
const studentScores = students
.map(student => allScores.get(student))
.filter(score => !!score);
return studentScores.reduce((a, b) => a + b) / studentScores.length;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// error: Object is possibly 'undefined'.
}

TypeScript 没有为 score => !!score 推断出类型谓词,这是合理的:如果它返回 true,那么 scorenumber。 但如果它返回 false,那么 score 可能是 undefinednumber(具体来说是 0)。 这是一个真正的漏洞:如果有学生在测试中得了零分,那么过滤掉他们的分数会使平均值偏高。 更少的人会高于平均水平,而更多的人会感到难过!

🌐 TypeScript did not infer a type predicate for score => !!score, and rightly so: if this returns true then score is a number. But if it returns false, then score could be either undefined or a number (specifically, 0). This is a real bug: if any student got a zero on the test, then filtering out their score will skew the average upwards. Fewer will be above average and more will be sad!

像第一个例子一样,最好显式地过滤掉 undefined 值:

🌐 As with the first example, it’s better to explicitly filter out undefined values:

tsx
function getClassroomAverage(students: string[], allScores: Map<string, number>) {
const studentScores = students
.map(student => allScores.get(student))
.filter(score => score !== undefined);
return studentScores.reduce((a, b) => a + b) / studentScores.length; // ok!
}

一个真实性检查 为对象类型推断类型谓词,在没有歧义的情况下。记住,函数必须返回 boolean 才能成为推断类型谓词的候选:x => !!x 可能会推断类型谓词,但 x => x 肯定不会。

🌐 A truthiness check will infer a type predicate for object types, where there’s no ambiguity. Remember that functions must return a boolean to be a candidate for an inferred type predicate: x => !!x might infer a type predicate, but x => x definitely won’t.

显式类型谓词的工作方式与以前完全相同。 TypeScript 不会检查它是否会推断出相同的类型谓词。 显式类型谓词(“is”)并不比类型断言(“as”)更安全。

🌐 Explicit type predicates continue to work exactly as before. TypeScript will not check whether it would infer the same type predicate. Explicit type predicates (“is”) are no safer than a type assertion (“as”).

如果 TypeScript 现在推断出的类型比你想要的更精确,这个功能可能会破坏现有代码。例如:

🌐 It’s possible that this feature will break existing code if TypeScript now infers a more precise type than you want. For example:

tsx
// Previously, nums: (number | null)[]
// Now, nums: number[]
const nums = [1, 2, 3, null, 5].filter(x => x !== null);
nums.push(null); // ok in TS 5.4, error in TS 5.5

修复方法是使用显式类型注解告诉 TypeScript 你想要的类型:

🌐 The fix is to tell TypeScript the type that you want using an explicit type annotation:

tsx
const nums: (number | null)[] = [1, 2, 3, null, 5].filter(x => x !== null);
nums.push(null); // ok in all versions

欲了解更多信息,请查看 实现拉取请求Dan 关于实现此功能的博客文章

🌐 For more information, check out the implementing pull request and Dan’s blog post about implementing this feature.

针对常量索引访问的控制流收窄

🌐 Control Flow Narrowing for Constant Indexed Accesses

objkey 都实际上是常量时,TypeScript 现在能够缩小 obj[key] 形式的表达式。

🌐 TypeScript is now able to narrow expressions of the form obj[key] when both obj and key are effectively constant.

ts
function f1(obj: Record<string, unknown>, key: string) {
if (typeof obj[key] === "string") {
// Now okay, previously was error
obj[key].toUpperCase();
}
}

在上面的例子中,objkey 都不会被修改,因此 TypeScript 可以在 typeof 检查之后将 obj[key] 的类型缩小到 string

更多信息,请参见此处的实现拉取请求

🌐 In the above, neither obj nor key are ever mutated, so TypeScript can narrow the type of obj[key] to string after the typeof check. For more information, see the implementing pull request here.

JSDoc @import 标签

🌐 The JSDoc @import Tag

今天,如果你只想在 JavaScript 文件中为了类型检查而导入某些东西,这是很麻烦的。JavaScript 开发者不能简单地导入一个名为 SomeType 的类型,如果它在运行时不存在。

🌐 Today, if you want to import something only for type-checking in a JavaScript file, it is cumbersome. JavaScript developers can’t simply import a type named SomeType if it’s not there at runtime.

js
// ./some-module.d.ts
export interface SomeType {
// ...
}
// ./index.js
import { SomeType } from "./some-module"; // ❌ runtime error!
/**
* @param {SomeType} myValue
*/
function doSomething(myValue) {
// ...
}

SomeType 在运行时不会存在,因此导入会失败。开发者可以改为使用命名空间导入。

js
import * as someModule from "./some-module";
/**
* @param {someModule.SomeType} myValue
*/
function doSomething(myValue) {
// ...
}

但是 ./some-module 在运行时仍然会被导入——这可能也不是理想的情况。

🌐 But ./some-module is still imported at runtime - which might also not be desirable.

为了避免这种情况,开发者通常需要在 JSDoc 注释中使用 import(...) 类型。

🌐 To avoid this, developers typically had to use import(...) types in JSDoc comments.

js
/**
* @param {import("./some-module").SomeType} myValue
*/
function doSomething(myValue) {
// ...
}

如果你想在多个地方重复使用相同类型,你可以使用 typedef 来避免重复导入。

🌐 If you wanted to reuse the same type in multiple places, you could use a typedef to avoid repeating the import.

js
/**
* @typedef {import("./some-module").SomeType} SomeType
*/
/**
* @param {SomeType} myValue
*/
function doSomething(myValue) {
// ...
}

这有助于本地使用 SomeType,但对于许多导入来说会显得重复,而且有点冗长。

🌐 This helps with local uses of SomeType, but it gets repetitive for many imports and can be a bit verbose.

这就是为什么 TypeScript 现在支持一个新的 @import 注释标签,它的语法与 ECMAScript 导入相同。

🌐 That’s why TypeScript now supports a new @import comment tag that has the same syntax as ECMAScript imports.

js
/** @import { SomeType } from "some-module" */
/**
* @param {SomeType} myValue
*/
function doSomething(myValue) {
// ...
}

在这里,我们使用了命名导入。我们也可以将导入写成命名空间导入。

🌐 Here, we used named imports. We could also have written our import as a namespace import.

js
/** @import * as someModule from "some-module" */
/**
* @param {someModule.SomeType} myValue
*/
function doSomething(myValue) {
// ...
}

因为这些只是 JSDoc 注释,所以它们根本不影响运行时行为。

🌐 Because these are just JSDoc comments, they don’t affect runtime behavior at all.

我们要向 Oleksandr Tarasiuk 表示衷心的感谢,他贡献了 这个更改

🌐 We would like to extend a big thanks to Oleksandr Tarasiuk who contributed this change!

正则表达式语法检查

🌐 Regular Expression Syntax Checking

直到现在,TypeScript 通常都会忽略代码中的大多数正则表达式。这是因为正则表达式从技术上来说具有可扩展的语法,而 TypeScript 从未尝试将正则表达式编译到较早版本的 JavaScript。然而,这意味着许多常见问题在正则表达式中可能不会被发现,它们要么在运行时变成错误,要么悄无声息地失败。

🌐 Until now, TypeScript has typically skipped over most regular expressions in code. This is because regular expressions technically have an extensible grammar and TypeScript never made any effort to compile regular expressions to earlier versions of JavaScript. Still, this meant that lots of common problems would go undiscovered in regular expressions, and they would either turn into errors at runtime, or silently fail.

但 TypeScript 现在会对正则表达式进行基本的语法检查!

🌐 But TypeScript now does basic syntax checking on regular expressions!

ts
let myRegex = /@robot(\s+(please|immediately)))? do some task/;
// ~
// error!
// Unexpected ')'. Did you mean to escape it with backslash?

这是一个简单的例子,但这种检查可以发现许多常见错误。事实上,TypeScript 的检查甚至超出了语法检查的范围。例如,TypeScript 现在可以捕捉到不存在的反向引用相关的问题。

🌐 This is a simple example, but this checking can catch a lot of common mistakes. In fact, TypeScript’s checking goes slightly beyond syntactic checks. For instance, TypeScript can now catch issues around backreferences that don’t exist.

ts
let myRegex = /@typedef \{import\((.+)\)\.([a-zA-Z_]+)\} \3/u;
// ~
// error!
// This backreference refers to a group that does not exist.
// There are only 2 capturing groups in this regular expression.

这同样适用于命名捕获组。

🌐 The same applies to named capturing groups.

ts
let myRegex = /@typedef \{import\((?<importPath>.+)\)\.(?<importedEntity>[a-zA-Z_]+)\} \k<namedImport>/;
// ~~~~~~~~~~~
// error!
// There is no capturing group named 'namedImport' in this regular expression.

TypeScript 的检查现在也能检测到何时使用了比你的目标 ECMAScript 版本更新的某些 RegExp 特性。例如,如果我们在 ES5 目标中使用上面那样的命名捕获组,就会收到错误提示。

🌐 TypeScript’s checking is now also aware of when certain RegExp features are used when newer than your target version of ECMAScript. For example, if we use named capturing groups like the above in an ES5 target, we’ll get an error.

ts
let myRegex = /@typedef \{import\((?<importPath>.+)\)\.(?<importedEntity>[a-zA-Z_]+)\} \k<importedEntity>/;
// ~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
// error!
// Named capturing groups are only available when targeting 'ES2018' or later.

某些正则表达式标志也是如此。

🌐 The same is true for certain regular expression flags as well.

请注意,TypeScript 对正则表达式的支持仅限于正则表达式字面量。如果你尝试使用字符串字面量调用 new RegExp,TypeScript 将不会检查提供的字符串。

🌐 Note that TypeScript’s regular expression support is limited to regular expression literals. If you try calling new RegExp with a string literal, TypeScript will not check the provided string.

我们要感谢 GitHub 用户 graphemecluster,他与我们反复尝试,将此功能引入 TypeScript

🌐 We would like to thank GitHub user graphemecluster who iterated a ton with us to get this feature into TypeScript.

支持新的 ECMAScript Set 方法

🌐 Support for New ECMAScript Set Methods

TypeScript 5.5 为 ECMAScript Set 类型声明了新的提议方法

🌐 TypeScript 5.5 declares new proposed methods for the ECMAScript Set type.

其中一些方法,如 unionintersectiondifferencesymmetricDifference,接受另一个 Set 并返回一个新的 Set 作为结果。其他方法,如 isSubsetOfisSupersetOfisDisjointFrom,接受另一个 Set 并返回一个 boolean。这些方法都不会修改原来的 Set

🌐 Some of these methods, like union, intersection, difference, and symmetricDifference, take another Set and return a new Set as the result. The other methods, isSubsetOf, isSupersetOf, and isDisjointFrom, take another Set and return a boolean. None of these methods mutate the original Sets.

以下是一个简单的示例,说明如何使用这些方法以及它们的行为方式:

🌐 Here’s a quick example of how you might use these methods and how they behave:

ts
let fruits = new Set(["apples", "bananas", "pears", "oranges"]);
let applesAndBananas = new Set(["apples", "bananas"]);
let applesAndOranges = new Set(["apples", "oranges"]);
let oranges = new Set(["oranges"]);
let emptySet = new Set();
////
// union
////
// Set(4) {'apples', 'bananas', 'pears', 'oranges'}
console.log(fruits.union(oranges));
// Set(3) {'apples', 'bananas', 'oranges'}
console.log(applesAndBananas.union(oranges));
////
// intersection
////
// Set(2) {'apples', 'bananas'}
console.log(fruits.intersection(applesAndBananas));
// Set(0) {}
console.log(applesAndBananas.intersection(oranges));
// Set(1) {'apples'}
console.log(applesAndBananas.intersection(applesAndOranges));
////
// difference
////
// Set(3) {'apples', 'bananas', 'pears'}
console.log(fruits.difference(oranges));
// Set(2) {'pears', 'oranges'}
console.log(fruits.difference(applesAndBananas));
// Set(1) {'bananas'}
console.log(applesAndBananas.difference(applesAndOranges));
////
// symmetricDifference
////
// Set(2) {'bananas', 'oranges'}
console.log(applesAndBananas.symmetricDifference(applesAndOranges)); // no apples
////
// isDisjointFrom
////
// true
console.log(applesAndBananas.isDisjointFrom(oranges));
// false
console.log(applesAndBananas.isDisjointFrom(applesAndOranges));
// true
console.log(fruits.isDisjointFrom(emptySet));
// true
console.log(emptySet.isDisjointFrom(emptySet));
////
// isSubsetOf
////
// true
console.log(applesAndBananas.isSubsetOf(fruits));
// false
console.log(fruits.isSubsetOf(applesAndBananas));
// false
console.log(applesAndBananas.isSubsetOf(oranges));
// true
console.log(fruits.isSubsetOf(fruits));
// true
console.log(emptySet.isSubsetOf(fruits));
////
// isSupersetOf
////
// true
console.log(fruits.isSupersetOf(applesAndBananas));
// false
console.log(applesAndBananas.isSupersetOf(fruits));
// false
console.log(applesAndBananas.isSupersetOf(oranges));
// true
console.log(fruits.isSupersetOf(fruits));
// false
console.log(emptySet.isSupersetOf(fruits));

我们要感谢 Kevin Gibbons,他不仅在 ECMAScript 中共同推动了该功能,还为 TypeScript 中的 SetReadonlySetReadonlySetLike 提供了声明

🌐 We’d like to thank Kevin Gibbons who not only co-championed the feature in ECMAScript, but also provided the declarations for Set, ReadonlySet, and ReadonlySetLike in TypeScript!

孤立声明

🌐 Isolated Declarations

本部分由 Rob Palmer 共同撰写,他支持了孤立声明的设计。

声明文件(也称为 .d.ts 文件)向 TypeScript 描述现有库和模块的结构。这个轻量级的描述包含了库的类型签名,但不包括函数体等实现细节。它们的发布是为了让 TypeScript 能够高效地检查你对库的使用,而无需分析库本身。虽然可以手动编写声明文件,但如果你正在编写类型化代码,更安全、更简单的做法是让 TypeScript 使用 --declaration 从源文件自动生成它们。

🌐 Declaration files (a.k.a. .d.ts files) describe the shape of existing libraries and modules to TypeScript. This lightweight description includes the library’s type signatures and excludes implementation details such as the function bodies. They are published so that TypeScript can efficiently check your usage of a library without needing to analyse the library itself. Whilst it is possible to handwrite declaration files, if you are authoring typed code, it’s much safer and simpler to let TypeScript generate them automatically from source files using --declaration.

TypeScript 编译器及其 API 一直负责生成声明文件;然而,有些情况下你可能希望使用其他工具,或者传统的构建过程无法扩展。

🌐 The TypeScript compiler and its APIs have always had the job of generating declaration files; however, there are some use-cases where you might want to use other tools, or where the traditional build process doesn’t scale.

使用场景:更快速的声明生成工具

🌐 Use-case: Faster Declaration Emit Tools

想象一下,如果你想创建一个更快速的工具来生成声明文件,也许是作为发布服务或新打包工具的一部分。虽然有一个蓬勃发展的生态系统,提供将 TypeScript 转换为 JavaScript 的超高速工具,但将 TypeScript 转换为声明文件的情况并非如此。原因是 TypeScript 的类型推断允许我们在不显式声明类型的情况下编写代码,这意味着声明文件的生成可能会很复杂。

🌐 Imagine if you wanted to create a faster tool to generate declaration files, perhaps as part of a publishing service or a new bundler. Whilst there is a thriving ecosystem of blazing fast tools that can turn TypeScript into JavaScript, the same is not true for turning TypeScript into declaration files. The reason is that TypeScript’s inference allows us to write code without explicitly declaring types, meaning declaration emit can be complex.

让我们考虑一个简单的函数示例,该函数将添加两个导入的变量。

🌐 Let’s consider a simple example of a function that adds two imported variables.

ts
// util.ts
export let one = "1";
export let two = "2";
// add.ts
import { one, two } from "./util";
export function add() { return one + two; }

即使我们唯一想做的只是生成 add.d.ts,TypeScript 仍然需要进入另一个导入的文件(util.ts),推断出 onetwo 的类型是字符串,然后计算两个字符串上的 + 操作将会生成 string 返回类型。

🌐 Even if the only thing we want to do is generate add.d.ts, TypeScript needs to crawl into another imported file (util.ts), infer that the type of one and two are strings, and then calculate that the + operator on two strings will lead to a string return type.

ts
// add.d.ts
export declare function add(): string;

虽然这种推断对于开发者体验很重要,但这意味着想要生成声明文件的工具需要复制类型检查器的部分功能,包括推断以及解析模块说明符以遵循导入的功能。

🌐 While this inference is important for the developer experience, it means that tools that want to generate declaration files would need to replicate parts of the type-checker including inference and the ability to resolve module specifiers to follow the imports.

用例:并行声明触发与并行检查

🌐 Use-case: Parallel Declaration Emit and Parallel Checking

想象一下,如果你有一个包含许多项目的单一代码仓库,并且有一个多核CPU,它希望能够帮助你更快地检查代码。如果我们能够同时检查所有这些项目,让每个项目在不同的核心上运行,那该多好啊?

🌐 Imagine if you had a monorepo containing many projects and a multi-core CPU that just wished it could help you check your code faster. Wouldn’t it be great if we could check all those projects at the same time by running each project on a different core?

不幸的是,我们不能自由地并行执行所有的工作。原因是我们必须按照依赖顺序来构建这些项目,因为每个项目都要检查其依赖的声明文件。因此,我们必须先构建依赖以生成声明文件。TypeScript 的项目引用功能也是这样工作,按“拓扑”依赖顺序构建项目集合。

🌐 Unfortunately we don’t have the freedom to do all the work in parallel. The reason is that we have to build those projects in dependency order, because each project is checking against the declaration files of their dependencies. So we must build the dependency first to generate the declaration files. TypeScript’s project references feature works the same way, building the set of projects in “topological” dependency order.

例如,如果我们有两个名为 backendfrontend 的项目,它们都依赖于一个名为 core 的项目,那么 TypeScript 在 core 被构建并生成其声明文件之前,无法开始对 frontendbackend 进行类型检查。

🌐 As an example, if we have two projects called backend and frontend, and they both depend on a project called core, TypeScript can’t start type-checking either frontend or backend until core has been built and its declaration files have been generated.

frontend and backend point to core, other stuff might point to each of those

在上图中,你可以看到我们有一个瓶颈。虽然我们可以并行构建 frontendbackend,但在开始构建其中任何一个之前,我们需要先等待 core 构建完成。

🌐 In the above graph, you can see that we have a bottleneck. Whilst we can build frontend and backend in parallel, we need to first wait for core to finish building before either can start.

我们怎么改进这个呢? 嗯,如果有一个快速工具可以并行地生成 core 的所有声明文件,那么 TypeScript 就可以紧接着并行地corefrontendbackend 进行类型检查。

🌐 How could we improve upon this? Well, if a fast tool could generate all those declaration files for core in parallel, TypeScript then could immediately follow that by type-checking core, frontend, and backend also in parallel.

解决方案:显式类型!

🌐 Solution: Explicit Types!

这两种使用场景的共同要求是我们需要一个跨文件的类型检查器来生成声明文件。这对工具社区来说是一个很大的要求。

🌐 The common requirement in both use-cases is that we need a cross-file type-checker to generate declaration files. Which is a lot to ask from the tooling community.

举一个更复杂的例子,如果我们想要一个包含以下代码的声明文件……

🌐 As a more complex example, if we want a declaration file for the following code…

ts
import { add } from "./add";
const x = add();
export function foo() {
return x;
}

…我们需要为 foo 生成一个签名。
这需要查看 foo 的实现。
foo 只是返回 x,所以要获取 x 的类型,需要查看 add 的实现。
但那可能还需要查看 add 的依赖实现,依此类推。
我们在这里看到的是,生成声明文件需要很多逻辑来确定不同地方的类型,而这些地方可能甚至不在当前文件中。

然而,对于希望快速迭代并进行完全并行构建的开发者来说,还有另一种思考这个问题的方式。声明文件只需要模块公共 API 的类型——换句话说,就是导出内容的类型。如果有争议的是,开发者愿意明确地写出他们导出内容的类型,工具就可以在不查看模块实现的情况下生成声明文件——并且无需重新实现完整的类型检查器。

🌐 Still, for developers looking for fast iteration time and fully parallel builds, there is another way of thinking about this problem. A declaration file only requires the types of the public API of a module - in other words, the types of the things that are exported. If, controversially, developers are willing to explicitly write out the types of the things they export, tools could generate declaration files without needing to look at the implementation of the module - and without reimplementing a full type-checker.

这就是新的 --isolatedDeclarations 选项发挥作用的地方。 当一个模块在没有类型检查器的情况下无法可靠地转换时,--isolatedDeclarations 会报告错误。 更简单地说,如果你的文件在导出上没有足够的注释,它会让 TypeScript 报告错误。

🌐 This is where the new --isolatedDeclarations option comes in. --isolatedDeclarations reports errors when a module can’t be reliably transformed without a type-checker. More plainly, it makes TypeScript report errors if you have a file that isn’t sufficiently annotated on its exports.

这意味着在上面的例子中,我们会看到如下错误:

🌐 That means in the above example, we would see an error like the following:

ts
export function foo() {
// ~~~
// error! Function must have an explicit
// return type annotation with --isolatedDeclarations.
return x;
}

为什么错误是可取的?

🌐 Why are errors desirable?

这意味着 TypeScript 可以

🌐 Because it means that TypeScript can

  1. 请提前告知我们其他工具在生成声明文件时是否会遇到问题。
  2. 提供快速修复以帮助添加这些缺失的注释。

不过,这种模式并不要求到处都加注解。对于本地代码来说,这些注解可以忽略,因为它们不会影响公共 API。例如,以下代码不会产生错误:

🌐 This mode doesn’t require annotations everywhere though. For locals, these can be ignored, since they don’t affect the public API. For example, the following code would not produce an error:

ts
import { add } from "./add";
const x = add("1", "2"); // no error on 'x', it's not exported.
export function foo(): string {
return x;
}

也有一些表达式,其类型“平凡”到可以计算。

🌐 There are also certain expressions where the type is “trivial” to calculate.

ts
// No error on 'x'.
// It's trivial to calculate the type is 'number'
export let x = 10;
// No error on 'y'.
// We can get the type from the return expression.
export function y() {
return 20;
}
// No error on 'z'.
// The type assertion makes it clear what the type is.
export function z() {
return Math.max(x, y()) as number;
}

使用 isolatedDeclarations

🌐 Using isolatedDeclarations

isolatedDeclarations 要求同时设置 declarationcomposite 标志之一。

请注意,isolatedDeclarations 并不会改变 TypeScript 的输出行为 —— 它只会影响错误报告的方式。重要的是,与 isolatedModules 类似,在 TypeScript 中启用该功能不会立即带来这里讨论的潜在好处。因此,请耐心等待,并期待该字段未来的发展。考虑到工具作者,我们还应该认识到,今天并非所有 TypeScript 的声明输出都能被其他希望将其作为参考的工具轻松复现。这是我们正在积极改进的方面。

🌐 Note that isolatedDeclarations does not change how TypeScript performs emit - just how it reports errors. Importantly, and similar to isolatedModules, enabling the feature in TypeScript won’t immediately bring about the potential benefits discussed here. So please be patient and look forward to future developments in this space. Keeping tool authors in mind, we should also recognize that today, not all of TypeScript’s declaration emit can be easily replicated by other tools wanting to use it as a guide. That’s something we’re actively working on improving.

除此之外,独立声明仍然是一个新功能,我们正在积极努力改善使用体验。 某些场景,例如在类和对象字面量中使用计算属性声明,在 isolatedDeclarations 下尚不支持。 请继续关注相关动态,并随时向我们提供反馈。

🌐 On top of this, isolated declarations are still a new feature, and we’re actively working on improving the experience. Some scenarios, like using computed property declarations in classes and object literals, are not yet supported under isolatedDeclarations. Keep an eye on this space, and feel free to provide us with feedback.

我们也认为值得指出的是,isolatedDeclarations 应该根据具体情况采用。使用 isolatedDeclarations 时会失去一些开发者体验,如果你的设置没有利用前面提到的两种情况,它可能不是合适的选择。对于其他情况,isolatedDeclarations 的工作已经发现了许多优化机会,并能启用不同的并行构建策略。同时,如果你愿意接受这些权衡,我们相信 isolatedDeclarations 可以成为加速构建过程的强大工具,尤其是在外部工具日益普及的情况下。

🌐 We also feel it is worth calling out that isolatedDeclarations should be adopted on a case-by-case basis. There are some developer ergonomics that are lost when using isolatedDeclarations, and thus it may not be the right choice if your setup is not leveraging the two scenarios mentioned earlier. For others, the work on isolatedDeclarations has already uncovered many optimizations and opportunities to unlock different parallel build strategies. In the meantime, if you’re willing to make the trade-offs, we believe isolatedDeclarations can be a powerful tool to speed up your build process as external tooling becomes more widely available.

欲了解更多信息,请查阅 TypeScript 问题追踪器上的 独立声明:功能状态 讨论。

🌐 For more information, read up on the Isolated Declarations: State of the Feature discussion on the TypeScript issue tracker.

信用

🌐 Credit

isolatedDeclarations 上的工作一直是 TypeScript 团队与 Bloomberg 和 Google 内部的基础设施及工具团队的长期合作努力。像 Google 的 Hana Joo 这样的人实现了针对孤立声明错误的快速修复(稍后会详细介绍),以及 Ashley Claymore、Jan Kühle、Lisa Velden、Rob Palmer 和 Thomas Chetwin 等人,也在数月的讨论、规范制定和实现过程中参与其中。但我们认为特别值得强调的是 Bloomberg 的 Titian Cernicova-Dragomir 所付出的巨大工作。Titian 对推动 isolatedDeclarations 的实现起到了关键作用,并且在此之前已经多年为 TypeScript 项目做出贡献。

🌐 Work on isolatedDeclarations has been a long-time collaborative effort between the TypeScript team and the infrastructure and tooling teams within Bloomberg and Google. Individuals like Hana Joo from Google who implemented the quick fix for isolated declaration errors (more on that soon), as well as Ashley Claymore, Jan Kühle, Lisa Velden, Rob Palmer, and Thomas Chetwin have been involved in discussion, specification, and implementation for many months. But we feel it is specifically worth calling out the tremendous amount of work provided by Titian Cernicova-Dragomir from Bloomberg. Titian has been instrumental in driving the implementation of isolatedDeclarations and has been a contributor to the TypeScript project for years prior.

虽然这个功能涉及许多更改,但你可以在此处查看孤立声明的核心工作

🌐 While the feature involved many changes, you can see the core work for Isolated Declarations here.

配置文件的 ${configDir} 模板变量

🌐 The ${configDir} Template Variable for Configuration Files

在许多代码库中,重用共享的 tsconfig.json 文件作为其他配置文件的“基础”是很常见的。 这是通过在 tsconfig.json 文件中使用 extends 字段来完成的。

🌐 It’s common in many codebases to reuse a shared tsconfig.json file that acts as a “base” for other configuration files. This is done by using the extends field in a tsconfig.json file.

json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist"
}
}

其中一个问题是,tsconfig.json 文件中的所有路径都是相对于文件本身的位置的。这意味着如果你有一个被多个项目使用的共享 tsconfig.base.json 文件,相对路径在派生项目中通常不会有用。例如,假设有如下的 tsconfig.base.json

🌐 One of the issues with this is that all paths in the tsconfig.json file are relative to the location of the file itself. This means that if you have a shared tsconfig.base.json file that is used by multiple projects, relative paths often won’t be useful in the derived projects. For example, imagine the following tsconfig.base.json:

json
{
"compilerOptions": {
"typeRoots": [
"./node_modules/@types",
"./custom-types"
],
"outDir": "dist"
}
}

如果作者的意图是每个扩展此文件的 tsconfig.json 都应该

🌐 If author’s intent was that every tsconfig.json that extends this file should

  1. 输出到相对于派生 tsconfig.jsondist 目录,并
  2. 相对于派生的 tsconfig.json 有一个 custom-types 目录,

那么这将行不通。 typeRoots 路径将相对于共享的 tsconfig.base.json 文件的位置,而不是扩展它的项目。 每个扩展此共享文件的项目都需要声明自己具有相同内容的 outDirtypeRoots。 这可能会令人沮丧,并且在项目之间保持同步会很困难,尽管上面的例子使用的是 typeRoots,但这对 paths 和其他选项也是一个常见问题。

🌐 then this would not work. The typeRoots paths would be relative to the location of the shared tsconfig.base.json file, not the project that extends it. Each project that extends this shared file would need to declare its own outDir and typeRoots with identical contents. This could be frustrating and hard to keep in sync between projects, and while the example above is using typeRoots, this is a common problem for paths and other options.

为了解决这个问题,TypeScript 5.5 引入了一个新的模板变量 ${configDir}。当在 tsconfig.jsonjsconfig.json 文件的某些路径字段中写入 ${configDir} 时,该变量会被替换为给定编译中配置文件所在的目录。这意味着上述 tsconfig.base.json 可以重写为:

🌐 To solve this, TypeScript 5.5 introduces a new template variable ${configDir}. When ${configDir} is written in certain path fields of a tsconfig.json or jsconfig.json files, this variable is substituted with the containing directory of the configuration file in a given compilation. This means that the above tsconfig.base.json could be rewritten as:

json
{
"compilerOptions": {
"typeRoots": [
"${configDir}/node_modules/@types",
"${configDir}/custom-types"
],
"outDir": "${configDir}/dist"
}
}

现在,当一个项目扩展这个文件时,路径将相对于派生的 tsconfig.json 文件,而不是共享的 tsconfig.base.json 文件。这样可以更轻松地在项目之间共享配置文件,并确保配置文件更具可移植性。

🌐 Now, when a project extends this file, the paths will be relative to the derived tsconfig.json, not the shared tsconfig.base.json file. This makes it easier to share configuration files across projects and ensures that the configuration files are more portable.

如果你打算让 tsconfig.json 文件可扩展,请考虑是否应该用 ${configDir} 来编写 ./

🌐 If you intend to make a tsconfig.json file extendable, consider if a ./ should instead be written with ${configDir}.

欲了解更多信息,请参阅提案问题实现拉取请求

🌐 For more information, see the proposal issue and the implementing pull request.

正在咨询 package.json 依赖以生成声明文件

🌐 Consulting package.json Dependencies for Declaration File Generation

以前,TypeScript 通常会触发如下错误消息:

🌐 Previously, TypeScript would often issue an error message like

The inferred type of "X" cannot be named without a reference to "Y". This is likely not portable. A type annotation is necessary.

这通常是因为 TypeScript 的声明文件生成过程中,会意外地处理程序中从未显式导入的文件内容。如果生成对这样的文件的导入,路径如果最终是相对路径,可能会有风险。不过,对于在 package.jsondependencies(或 peerDependenciesoptionalDependencies)中具有显式依赖的代码库,在某些解析模式下生成这样的导入应该是安全的。因此,在 TypeScript 5.5 中,如果是这种情况,我们会更加宽松,许多此类错误的出现应该会消失。

🌐 This was often due to TypeScript’s declaration file generation finding itself in the contents of files that were never explicitly imported in a program. Generating an import to such a file could be risky if the path ended up being relative. Still, for codebases with explicit dependencies in the dependencies (or peerDependencies and optionalDependencies) of a package.json, generating such an import should be safe under certain resolution modes. So in TypeScript 5.5, we’re more lenient when that’s the case, and many occurrences of this error should disappear.

查看此拉取请求 了解更改的详细信息。

编辑器和观察模式可靠性改进

🌐 Editor and Watch-Mode Reliability Improvements

TypeScript 要么添加了一些新功能,要么修复了现有逻辑,这使得 --watch 模式和 TypeScript 的编辑器集成感觉更加可靠。希望这能减少 TSServer/编辑器的重启次数。

🌐 TypeScript has either added some new functionality or fixed existing logic that makes --watch mode and TypeScript’s editor integration feel more reliable. That should hopefully translate to fewer TSServer/editor restarts.

正确刷新配置文件中的编辑器错误

🌐 Correctly Refresh Editor Errors in Configuration Files

TypeScript 可以为 tsconfig.json 文件生成错误;然而,这些错误实际上是从加载项目时产生的,编辑器通常不会直接请求 tsconfig.json 文件的这些错误。虽然这听起来像是技术细节,但这意味着当 tsconfig.json 中的所有错误都被修复后,TypeScript 并不会生成一组新的空错误集合,用户会保留下旧的错误,除非他们重新加载编辑器。

🌐 TypeScript can generate errors for tsconfig.json files; however, those errors are actually generated from loading a project, and editors typically don’t directly request those errors for tsconfig.json files. While this sounds like a technical detail, it means that when all errors issued in a tsconfig.json are fixed, TypeScript doesn’t issue a new fresh empty set of errors, and users are left with stale errors unless they reload their editor.

TypeScript 5.5 现在会故意触发一个事件来清除这些内容。 查看更多

🌐 TypeScript 5.5 now intentionally issues an event to clear these out. See more here.

改进对删除后立即写入的处理

🌐 Better Handling for Deletes Followed by Immediate Writes

有些工具不会覆盖文件,而是选择删除它们,然后从头创建新文件。例如,运行 npm ci 时就是这种情况。

🌐 Instead of overwriting files, some tools will opt to delete them and then create new files from scratch. This is the case when running npm ci, for instance.

虽然这对那些工具来说可能效率很高,但在 TypeScript 的编辑器场景中可能会出现问题,因为删除一个被监视的文件可能会同时销毁它及其所有传递依赖。快速连续地删除和创建文件可能会导致 TypeScript 完全拆除整个项目,然后再从头重建它。

🌐 While this can be efficient for those tools, it can be problematic for TypeScript’s editor scenarios where deleting a watched might dispose of it and all of its transitive dependencies. Deleting and creating a file in quick succession could lead to TypeScript tearing down an entire project and then rebuilding it from scratch.

TypeScript 5.5 现在采用了更细致的方法,通过保留已删除项目的部分内容,直到检测到新的创建事件。
这应该会让像 npm ci 这样的操作在 TypeScript 中运行得更顺畅。
在这里查看更多关于该方法的信息:更多信息

🌐 TypeScript 5.5 now has a more nuanced approach by keeping parts of a deleted project around until it picks up on a new creation event. This should make operations like npm ci work a lot better with TypeScript. See more information on the approach here.

符号链接在解析失败时会被跟踪

🌐 Symlinks are Tracked in Failed Resolutions

当 TypeScript 无法解析模块时,它仍然需要监视任何失败的查找路径,以防模块随后被添加。以前,对于符号链接目录,这一操作是没有执行的,这可能会在类似 monorepo 的场景中导致可靠性问题,例如在一个项目中构建时,但没有在另一个项目中被检测到。这个问题在 TypeScript 5.5 中应该已修复,这意味着你不需要那么频繁地重启编辑器。

🌐 When TypeScript fails to resolve a module, it will still need to watch for any failed lookup paths in case the module is added later. Previously this was not done for symlinked directories, which could cause reliability issues in monorepo-like scenarios when a build occurred in one project but was not witnessed in the other. This should be fixed in TypeScript 5.5, and means you won’t need to restart your editor as often.

在这里查看更多信息

项目引用有助于自动导入

🌐 Project References Contribute to Auto-Imports

自动导入不再要求在项目引用设置中对依赖目至少有一个显式导入。相反,自动导入补全应该可以在你在 tsconfig.jsonreferences 字段中列出的任何内容上正常工作。

🌐 Auto-imports no longer requires at least one explicit import to dependent projects in a project reference setup. Instead, auto-import completions should just work across anything you’ve listed in the references field of your tsconfig.json.

查看更多有关实现拉取请求的信息

性能与尺寸优化

🌐 Performance and Size Optimizations

语言服务和公共 API 中的单态化对象

🌐 Monomorphized Objects in Language Service and Public API

在 TypeScript 5.0 中,我们确保我们的 NodeSymbol 对象具有一致的属性集和一致的初始化顺序。这样做有助于减少不同操作中的多态性,从而让运行时能够更快地获取属性。

🌐 In TypeScript 5.0, we ensured that our Node and Symbol objects had a consistent set of properties with a consistent initialization order. Doing so helps reduce polymorphism in different operations, which allows runtimes to fetch properties more quickly.

通过进行这一更改,我们在编译器中看到了显著的速度提升;然而,这些更改大多数是针对我们数据结构的内部分配器进行的。语言服务以及 TypeScript 的公共 API 对某些对象使用的是一套不同的分配器。这使得 TypeScript 编译器变得更精简,因为仅用于语言服务的数据永远不会在编译器中使用。

🌐 By making this change, we witnessed impressive speed wins in the compiler; however, most of these changes were performed on internal allocators for our data structures. The language service, along with TypeScript’s public API, uses a different set of allocators for certain objects. This allowed the TypeScript compiler to be a bit leaner, as data used only for the language service would never be used in the compiler.

在 TypeScript 5.5 中,相同的单态化工作也已经应用于语言服务和公共 API。这意味着,你的编辑器体验以及任何使用 TypeScript API 的构建工具都会明显加快。实际上,在我们的基准测试中,当使用公共 TypeScript API 的分配器时,构建时间提升了 5-8%,而语言服务操作速度提升了 10-20%。虽然这确实意味着内存占用增加,但我们认为这种权衡是值得的,并希望找到方法来减少这部分内存开销。现在,整体操作应该会感觉更加流畅了。

🌐 In TypeScript 5.5, the same monomorphization work has been done for the language service and public API. What this means is that your editor experience, and any build tools that use the TypeScript API, will get a decent amount faster. In fact, in our benchmarks, we’ve seen a 5-8% speedup in build times when using the public TypeScript API’s allocators, and language service operations getting 10-20% faster. While this does imply an increase in memory, we believe that tradeoff is worth it and hope to find ways to reduce that memory overhead. Things should feel a lot snappier now.

欲了解更多信息,请点击此处查看更改

🌐 For more information, see the change here.

单态化控制流节点

🌐 Monomorphized Control Flow Nodes

在 TypeScript 5.5 中,控制流图的节点已被单态化,以便它们始终保持一致的形状。通过这样做,检查时间通常会减少约 1%.

🌐 In TypeScript 5.5, nodes of the control flow graph have been monomorphized so that they always hold a consistent shape. By doing so, check times will often be reduced by about 1%.

在这里查看此更改

我们的控制流图优化

🌐 Optimizations on our Control Flow Graph

在许多情况下,控制流分析会遍历一些不会提供任何新信息的节点。我们观察到,如果某些节点的前驱节点(或“支配节点”)不存在任何提前终止或效果,这些节点总是可以被跳过。因此,TypeScript 现在构建其控制流图时,会利用这一点,链接到一个 确实 能为控制流分析提供有价值信息的早期节点。这会生成一个更平坦的控制流图,从而使遍历更高效。这种优化带来了适度的收益,在某些代码库中构建时间最多可减少 2%。

🌐 In many cases, control flow analysis will traverse nodes that don’t provide any new information. We observed that in the absence of any early termination or effects in the antecedents (or “dominators”) of certain nodes meant that those nodes could always be skipped over. As such, TypeScript now constructs its control flow graphs to take advantage of this by linking to an earlier node that does provide interesting information for control flow analysis. This yields a flatter control flow graph, which can be more efficient to traverse. This optimization has yielded modest gains, but with up to 2% reductions in build time on certain codebases.

你可以在这里阅读更多

🌐 You can read more here.

跳过 transpileModuletranspileDeclaration 的检查

🌐 Skipped Checking in transpileModule and transpileDeclaration

TypeScript 的 transpileModule API 可用于将单个 TypeScript 文件的内容编译为 JavaScript。 同样,transpileDeclaration API(见下文)可用于为单个 TypeScript 文件生成声明文件。 这些 API 的一个问题是,TypeScript 在输出之前会对文件的全部内容执行完整的类型检查。 这是必要的,以便收集稍后在触发阶段使用的某些信息。

🌐 TypeScript’s transpileModule API can be used for compiling a single TypeScript file’s contents into JavaScript. Similarly, the transpileDeclaration API (see below) can be used to generate a declaration file for a single TypeScript file. One of the issues with these APIs is that TypeScript internally would perform a full type-checking pass over the entire contents of the file before emitting the output. This was necessary to collect certain information which would later be used for the emit phase.

在 TypeScript 5.5 中,我们找到了一种方法,可以避免进行全面检查,仅在必要时懒惰地收集这些信息,并且 transpileModuletranspileDeclaration 默认都启用此功能。 因此,与这些 API 集成的工具,例如使用 transpileOnlyts-loaderts-jest,应该会看到明显的速度提升。 在我们的测试中,使用 transpileModule 通常可以见到构建时间约加快 2 倍

🌐 In TypeScript 5.5, we’ve found a way to avoid performing a full check, only lazily collecting this information as necessary, and transpileModule and transpileDeclaration both enable this functionality by default. As a result, tools that integrate with these APIs, like ts-loader with transpileOnly and ts-jest, should see a noticeable speedup. In our testing, we generally witness around a 2x speed-up in build time using transpileModule.

TypeScript 包大小缩减

🌐 TypeScript Package Size Reduction

通过进一步利用我们在 5.0 版本中向模块的过渡,我们显著减少了 TypeScript 的整体包大小,方法是让 tsserver.jstypingsInstaller.js 从一个公共 API 库中导入,而不是让它们各自生成独立的打包包

🌐 Further leveraging our transition to modules in 5.0, we’ve significantly reduced TypeScript’s overall package size by making tsserver.js and typingsInstaller.js import from a common API library instead of having each of them produce standalone bundles.

这将 TypeScript 的磁盘占用大小从 30.2 MB 减少到 20.4 MB,打包后大小从 5.5 MB 减少到 3.7 MB!

🌐 This reduces TypeScript’s size on disk from 30.2 MB to 20.4 MB, and reduces its packed size from 5.5 MB to 3.7 MB!

声明生成中的节点重用

🌐 Node Reuse in Declaration Emit

作为启用 isolatedDeclarations 工作的一部分,我们大幅提高了 TypeScript 在生成声明文件时直接复制你的输入源代码的频率。

🌐 As part of the work to enable isolatedDeclarations, we’ve substantially improved how often TypeScript can directly copy your input source code when producing declaration files.

例如,假设你写了

🌐 For example, let’s say you wrote

ts
export const strBool: string | boolean = "hello";
export const boolStr: boolean | string = "world";

请注意,联合类型是等价的,但联合的顺序不同。生成声明文件时,TypeScript 有两种等价的输出可能性。

🌐 Note that the union types are equivalent, but the order of the union is different. When emitting the declaration file, TypeScript has two equivalent output possibilities.

首先,为每种类型使用一致的规范表示:

🌐 The first is to use a consistent canonical representation for each type:

ts
export const strBool: string | boolean;
export const boolStr: string | boolean;

第二个变化是完全按照编写方式复用类型注释:

🌐 The second is to re-use the type annotations exactly as written:

ts
export const strBool: string | boolean;
export const boolStr: boolean | string;

第二种方法通常更可取,原因如下:

🌐 The second approach is generally preferable for a few reasons:

  • 许多等效表示仍然编码了某种程度的意图,最好将其保留在声明文件中。
  • 生成类型的全新表示可能成本较高,因此最好避免。
  • 用户编写的类型通常比生成的类型表示更短。

在 5.5 版本中,我们大幅提高了 TypeScript 正确识别可以安全且准确地将类型按原样打印回输入文件的地方的能力。
很多情况下这是无形的性能提升——TypeScript 之前会生成全新的语法节点集合并将它们序列化为字符串。
现在,TypeScript 可以直接操作原始语法节点,这样成本更低,速度更快。

🌐 In 5.5, we’ve greatly improved the number of places where TypeScript can correctly identify places where it’s safe and correct to print back types exactly as they were written in the input file. Many of these cases are invisible performance improvements - TypeScript would generate fresh sets of syntax nodes and serialize them into a string. Instead, TypeScript can now operate over the original syntax nodes directly, which is much cheaper and faster.

从判别联合缓存上下文类型

🌐 Caching Contextual Types from Discriminated Unions

当 TypeScript 需要获取一个表达式(如对象字面量)的上下文类型时,它通常会遇到一个联合类型。
在这些情况下,TypeScript 会尝试根据已知的具有已知值的属性(即判别属性)来过滤联合类型的成员。
这项工作可能相当耗费资源,尤其是当你最终得到一个由许多属性组成的对象时。
在 TypeScript 5.5 中,大部分计算会被缓存一次,从而不需要对对象字面量中的每个属性重新计算
执行这项优化使得编译 TypeScript 编译器本身的时间减少了 250 毫秒。

🌐 When TypeScript asks for the contextual type of an expression like an object literal, it will often encounter a union type. In those cases, TypeScript tries to filter out members of the union based on known properties with well known values (i.e. discriminant properties). This work can be fairly expensive, especially if you end up with an object consisting of many many properties. In TypeScript 5.5, much of the computation is cached once so that TypeScript doesn’t need to recompute it for every property in the object literal. Performing this optimization shaved 250ms off of compiling the TypeScript compiler itself.

从 ECMAScript 模块更轻松地使用 API

🌐 Easier API Consumption from ECMAScript Modules

以前,如果你在 Node.js 中编写 ECMAScript 模块,无法从 typescript 包中使用命名导入。

🌐 Previously, if you were writing an ECMAScript module in Node.js, named imports were not available from the typescript package.

ts
import { createSourceFile } from "typescript"; // ❌ error
import * as ts from "typescript";
ts.createSourceFile // ❌ undefined???
ts.default.createSourceFile // ✅ works - but ugh!

这是因为 cjs-module-lexer 没有识别出 TypeScript 生成的 CommonJS 代码的模式。这个问题已经修复,现在用户可以在 Node.js 中使用 ECMAScript 模块从 TypeScript npm 包进行命名导入。

🌐 This is because cjs-module-lexer did not recognize the pattern of TypeScript’s generated CommonJS code. This has been fixed, and users can now use named imports from the TypeScript npm package with ECMAScript modules in Node.js.

ts
import { createSourceFile } from "typescript"; // ✅ works now!
import * as ts from "typescript";
ts.createSourceFile // ✅ works now!

欲了解更多信息,请点击此处查看更改

🌐 For more information, see the change here.

transpileDeclaration API

🌐 The transpileDeclaration API

TypeScript 的 API 提供了一个名为 transpileModule 的函数。它的目的是方便编译单个 TypeScript 文件。由于它无法访问整个 程序,因此如果代码在 isolatedModules 选项下有任何错误,它可能不会产生正确的输出,这是一个需要注意的问题。

🌐 TypeScript’s API exposes a function called transpileModule. It’s intended to make it easy to compile a single file of TypeScript code. Because it doesn’t have access to an entire program, the caveat is that it may not produce the right output if the code violates any errors under the isolatedModules option.

在 TypeScript 5.5 中,我们添加了一个名为 transpileDeclaration 的新类似 API。这个 API 类似于 transpileModule,但它专门用于根据一些输入源文本生成单个声明文件。就像 transpileModule 一样,它无法访问完整的程序,并且有类似的注意事项:只有在输入代码在新的 isolatedDeclarations 选项下没有错误时,它才会生成准确的声明文件。

🌐 In TypeScript 5.5, we’ve added a new similar API called transpileDeclaration. This API is similar to transpileModule, but it’s specifically designed to generate a single declaration file based on some input source text. Just like transpileModule, it doesn’t have access to a full program, and a similar caveat applies: it only generates an accurate declaration file if the input code is free of errors under the new isolatedDeclarations option.

如果需要,可以使用此功能在 isolatedDeclarations 模式下将声明生成在所有文件中并行化。

🌐 If desired, this function can be used to parallelize declaration emit across all files under isolatedDeclarations mode.

欲了解更多信息,请参见此处的实现

🌐 For more information, see the implementation here.

值得注意的行为变更

🌐 Notable Behavioral Changes

本节重点介绍了一些值得注意的变化,这些变化在任何升级中都应予以认可和理解。有时,它会高亮已弃用、移除和新的限制。它还可能包含作为功能性改进的错误修复,但这些修复也可能通过引入新的错误影响现有构建。

🌐 This section highlights a set of noteworthy changes that should be acknowledged and understood as part of any upgrade. Sometimes it will highlight deprecations, removals, and new restrictions. It can also contain bug fixes that are functionally improvements, but which can also affect an existing build by introducing new errors.

在 TypeScript 5.0 中弃用的功能禁用

🌐 Disabling Features Deprecated in TypeScript 5.0

TypeScript 5.0 弃用了以下选项和行为:

🌐 TypeScript 5.0 deprecated the following options and behaviors:

  • charset
  • target: ES3
  • importsNotUsedAsValues
  • noImplicitUseStrict
  • noStrictGenericChecks
  • keyofStringsOnly
  • suppressExcessPropertyErrors
  • suppressImplicitAnyIndexErrors
  • out
  • preserveValueImports
  • 项目引用中的 prepend
  • 隐式操作系统特定的 newLine

为了继续使用上述已弃用的选项,使用 TypeScript 5.0 及其他更高版本的开发者必须指定一个名为 ignoreDeprecations、值为 "5.0" 的新选项。

🌐 To continue using the deprecated options above, developers using TypeScript 5.0 and other more recent versions have had to specify a new option called ignoreDeprecations with the value "5.0".

在 TypeScript 5.5 中,这些选项不再起作用。为了帮助顺利升级,你仍然可以在 tsconfig 中指定它们,但在 TypeScript 6.0 中指定这些将会报错。另请参见 标志弃用计划,其中概述了我们的弃用策略。

🌐 In TypeScript 5.5, these options no longer have any effect. To help with a smooth upgrade path, you may still specify them in your tsconfig, but these will be an error to specify in TypeScript 6.0. See also the Flag Deprecation Plan which outlines our deprecation strategy.

有关这些弃用计划的更多信息,请参阅 GitHub,其中包含有关如何最好地调整代码库的建议。

lib.d.ts 变更

🌐 lib.d.ts Changes

为 DOM 生成的类型可能会影响对代码库的类型检查。更多信息,请参见 TypeScript 5.5 的 DOM 更新

🌐 Types generated for the DOM may have an impact on type-checking your codebase. For more information, see the DOM updates for TypeScript 5.5.

装饰器的更严格解析

🌐 Stricter Parsing for Decorators

自从 TypeScript 最初引入对装饰器的支持以来,该提案的指定语法已经收紧。TypeScript 现在对允许的形式更加严格。虽然罕见,但现有的装饰器可能需要加上圆括号以避免错误。

🌐 Since TypeScript originally introduced support for decorators, the specified grammar for the proposal has been tightened up. TypeScript is now stricter about what forms it allows. While rare, existing decorators may need to be parenthesized to avoid errors.

ts
class DecoratorProvider {
decorate(...args: any[]) { }
}
class D extends DecoratorProvider {
m() {
class C {
@super.decorate // ❌ error
method1() { }
@(super.decorate) // ✅ okay
method2() { }
}
}
}

请在此处查看有关更改的更多信息 [https://github.com/microsoft/TypeScript/pull/57749]。

🌐 See more information on the change here.

undefined 不再是可定义的类型名称

🌐 undefined is No Longer a Definable Type Name

TypeScript 始终不允许使用与内置类型冲突的类型别名:

🌐 TypeScript has always disallowed type alias names that conflict with built-in types:

ts
// Illegal
type null = any;
// Illegal
type number = any;
// Illegal
type object = any;
// Illegal
type any = any;

由于一个漏洞,这个逻辑并未同样适用于内置类型 undefined。 在 5.5 中,这现在被正确地识别为错误:

🌐 Due to a bug, this logic didn’t also apply to the built-in type undefined. In 5.5, this is now correctly identified as an error:

ts
// Now also illegal
type undefined = any;

对名为 undefined 的类型别名的直接引用从一开始就从未真正起作用。你可以定义它们,但不能将它们作为未限定的类型名称使用。

🌐 Bare references to type aliases named undefined never actually worked in the first place. You could define them, but you couldn’t use them as an unqualified type name.

ts
export type undefined = string;
export const m: undefined = "";
// ^
// Errors in 5.4 and earlier - the local definition of 'undefined' was not even consulted.

欲了解更多信息,请点击此处查看更改

🌐 For more information, see the change here.

简化引用指令声明触发

🌐 Simplified Reference Directive Declaration Emit

在生成声明文件时,当 TypeScript 认为需要时,它会合成一个引用指令。例如,所有 Node.js 模块都是以全局声明的方式声明的,因此仅通过模块解析无法加载。一个类似的文件:

🌐 When producing a declaration file, TypeScript would synthesize a reference directive when it believed one was required. For example, all Node.js modules are declared ambiently, so cannot be loaded by module resolution alone. A file like:

tsx
import path from "path";
export const myPath = path.parse(__filename);

会生成如下声明文件:

🌐 Would emit a declaration file like:

tsx
/// <reference types="node" />
import path from "path";
export declare const myPath: path.ParsedPath;

即使引用指令从未出现在原始源代码中。

🌐 Even though the reference directive never appeared in the original source.

同样地,TypeScript 也会移除它认为不需要包含在输出中的引用指令。例如,假设我们有一个指向 jest 的引用指令;然而,假设生成声明文件时该引用指令并不是必须的。TypeScript 会直接将其删除。因此在下面的示例中:

🌐 Similarly, TypeScript also removed reference directives that it did not believe needed to be a part of the output. For example, let’s imagine we had a reference directive to jest; however, imagine the reference directive isn’t necessary to generate the declaration file. TypeScript would simply drop it. So in the following example:

tsx
/// <reference types="jest" />
import path from "path";
export const myPath = path.parse(__filename);

TypeScript 仍然会触发:

🌐 TypeScript would still emit:

tsx
/// <reference types="node" />
import path from "path";
export declare const myPath: path.ParsedPath;

在进行 isolatedDeclarations 的工作过程中,我们意识到,对于任何试图在没有类型检查或只使用单个文件上下文的情况下实现声明生成器的人来说,这种逻辑都是站不住脚的。 这种行为从用户的角度来看也很难理解;除非你完全了解类型检查过程中发生的情况,否则在生成的文件中是否出现引用指令似乎是不一致且难以预测的。 为了避免在启用 isolatedDeclarations 时声明生成不同,我们知道我们的生成逻辑需要进行调整。

🌐 In the course of working on isolatedDeclarations, we realized that this logic was untenable for anyone attempting to implement a declaration emitter without type checking or using more than a single file’s context. This behavior is also hard to understand from a user’s perspective; whether or not a reference directive appeared in the emitted file seems inconsistent and difficult to predict unless you understand exactly what’s going on during typechecking. To prevent declaration emit from being different when isolatedDeclarations was enabled, we knew that our emit needed to change.

通过实验,我们发现几乎所有 TypeScript 自动生成引用指令的情况只是为了引入 nodereact。这些情况的预期是,下游用户已经通过 tsconfig.json "types" 或库导入引用了这些类型,因此不再生成这些引用指令不太可能影响任何人。值得注意的是,lib.d.ts 已经是这样工作的;当一个模块导出 WeakMap 时,TypeScript 不会自动生成对 lib="es2015" 的引用,而是假设下游用户会将其作为环境的一部分包含进去。

🌐 Through experimentation, we found that nearly all cases where TypeScript synthesized reference directives were just to pull in node or react. These are cases where the expectation is that a downstream user already references those types through tsconfig.json "types" or library imports, so no longer synthesizing these reference directives would be unlikely to break anyone. It’s worth noting that this is already how it works for lib.d.ts; TypeScript doesn’t synthesize a reference to lib="es2015" when a module exports a WeakMap, instead assuming that a downstream user will have included that as part of their environment.

对于由库作者编写的参考指令(非合成的),进一步实验表明几乎所有指令都被移除,从未出现在输出中。大多数被保留的参考指令都是损坏的,很可能并非原本打算保留下来。

🌐 For reference directives that had been written by library authors (not synthesized), further experimentation showed that nearly all were removed, never showing up in the output. Most reference directives that were preserved were broken and likely not intended to be preserved.

鉴于这些结果,我们决定在 TypeScript 5.5 中大幅简化声明生成中的引用指令。更一致的策略将帮助库的作者和使用者更好地控制他们的声明文件。

🌐 Given those results, we decided to greatly simplfy reference directives in declaration emit in TypeScript 5.5. A more consistent strategy will help library authors and consumers have better control of their declaration files.

引用指令不再被合成。 用户编写的引用指令不再被保留,除非使用新的 preserve="true" 属性进行注释。 具体来说,像这样的输入文件:

🌐 Reference directives are no longer synthesized. User-written reference directives are no longer preserved, unless annotated with a new preserve="true" attribute. Concretely, an input file like:

tsx
/// <reference types="some-lib" preserve="true" />
/// <reference types="jest" />
import path from "path";
export const myPath = path.parse(__filename);

将触发:

🌐 will emit:

tsx
/// <reference types="some-lib" preserve="true" />
import path from "path";
export declare const myPath: path.ParsedPath;

添加 preserve="true" 与较旧版本的 TypeScript 向后兼容,因为未知属性会被忽略。

🌐 Adding preserve="true" is backwards compatible with older versions of TypeScript as unknown attributes are ignored.

这一变化也提升了性能;在我们的基准测试中,对于启用了声明输出的项目,emit 阶段的性能提高了 1-4%。

🌐 This change also improved performance; in our benchmarks, the emit stage saw a 1-4% improvement in projects with declaration emit enabled.