TypeScript 5.5

推断类型谓词

¥Inferred Type Predicates

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

¥This section was written by Dan Vanderkam, who implemented this feature in TypeScript 5.5. Thanks 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 的类型声明支持类型谓词,因此最终结果是获得更精确的类型,代码也能通过类型检查器。

¥bird is Bird is the type predicate. It means that, if the function returns true, then it’s a Bird (if the function returns false then it’s undefined). The type declarations for Array.prototype.filter know about type predicates, so the net result is that you get a more precise type and the code passes the type checker.

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

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

  1. 该函数没有显式返回类型或类型谓词注解。

    ¥The function does not have an explicit return type or type predicate annotation.

  2. 该函数只有一个 return 语句,没有隐式返回。

    ¥The function has a single return statement and no implicit returns.

  3. 该函数不会改变其参数。

    ¥The function does not mutate its parameter.

  4. 该函数返回一个与参数细化相关的 boolean 表达式。

    ¥The function returns a boolean expression that’s tied to a refinement on the parameter.

通常情况下,这与你所期望的一样。以下是一些推断类型谓词的示例:

¥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

    ¥If the function returns true then x has the type T.

  2. 如果函数返回 false,则 x 不具有类型 T

    ¥If the function returns false then x does not have type 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,则 score 就是 number。但如果返回 false,则 score 可能是 undefinednumber(具体来说,0)。这是一个真正的 bug:如果任何学生在考试中得了零分,那么过滤掉他们的分数会导致平均分偏高。数量少了会高于平均水平,数量多了就不好了!

¥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 文件中导入某些内容进行类型检查,会非常繁琐。如果运行时不存在名为 SomeType 的类型,JavaScript 开发者无法简单地导入它。

¥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 在运行时不存在,因此导入将失败。开发者可以改用命名空间导入。

¥SomeType won’t exist at runtime, so the import will fail. Developers can instead use a namespace import instead.

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.

当版本高于目标 ECMAScript 版本时,TypeScript 的检查现在还可以识别某些 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 共同撰写。

¥This section was co-authored by Rob Palmer who supported the design of isolated declarations.

声明文件(又称 .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 的 monorepo,它只是希望它能帮助你更快地检查代码。如果我们能通过在不同的核心上运行每个项目来同时检查所有这些项目,那岂不是很棒?

¥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 的项目引用功能以相同的方式工作,按照 “topological” 依赖顺序构建项目集。

¥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 的项目,那么在 core 构建完成并生成其声明文件之前,TypeScript 无法开始对 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 依赖的实现等等。我们在这里看到的是,生成声明文件需要大量的逻辑来找出不同位置的类型,这些位置甚至可能不是当前文件的本地位置。

¥…we would need to generate a signature for foo. Well that requires looking at the implementation of foo. foo just returns x, so getting the type of x requires looking at the implementation of add. But that might require looking at the implementation of add’s dependencies, and so on. What we’re seeing here is that generating declaration files requires a whole lot of logic to figure out the types of different places that might not even be local to the current file.

不过,对于寻求快速迭代时间和完全并行构建的开发者来说,还有另一种思考这个问题的方法。声明文件只需要模块公共 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. 请提前告知我们其他工具在生成声明文件时是否会遇到问题。

    ¥Tell us up-front whether other tools will have issues with generating declaration files

  2. 提供快速修复以帮助添加这些缺失的注释。

    ¥Provide a quick fix to help add these missing annotations.

不过,此模式不需要在所有地方都使用注解。对于局部变量,这些可以忽略,因为它们不会影响公共 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;
}

某些表达式也需要使用 “trivial” 类型进行计算。

¥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 requires that either the declaration or composite flags are also set.

请注意,isolatedDeclarations 不会改变 TypeScript 执行 emit 的方式。 - 只是它如何报告错误。重要的是,与 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 团队与彭博和谷歌内部的基础设施和工具团队长期合作的成果。像 Google 的 Hana Joo(他实现了 快速修复独立声明错误,稍后会详细介绍)以及 Ashley Claymore、Jan Kühle、Lisa Velden、Rob Palmer 和 Thomas Chetwin 这样的个人,已经参与了讨论、规范和实现好几个月了。但是,我们认为特别值得一提的是彭博社为 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 文件作为其他配置文件的 “base” 是很常见的。这是通过在 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 目录,并且

    ¥output to a dist directory relative to the derived tsconfig.json , and

  2. 相对于派生的 tsconfig.json,有一个 custom-types 目录,

    ¥have a custom-types directory relative to the derived tsconfig.json,

那么这将行不通。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}。当 ${configDir} 写入 tsconfig.jsonjsconfig.json 文件的某些路径字段时,此变量将替换为给定编译中配置文件的包含目录。这意味着上面的 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.

有关此变更的更多详细信息,请参阅 查看此拉取请求

¥See this pull request for more details on the change.

编辑器和监视模式可靠性改进

¥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.

查看此处更多信息

¥See more information here.

项目引用 为自动导入做出贡献

¥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.

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

¥See more on the implementing pull request.

性能和大小优化

¥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%.

查看此处变更

¥See this change here.

控制流图的优化

¥Optimizations on our Control Flow Graph

在许多情况下,控制流分析将遍历不提供任何新信息的节点。我们观察到,如果某些节点的前件(或 “dominators”)中没有任何提前终止或影响,则意味着这些节点始终可以被跳过。因此,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 集成的工具(例如 ts-loadertranspileOnlyts-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 的整体包大小(通过从通用 API 库导入 tsserver.jstypingsInstaller.js,而不是让它们各自生成独立的包)。

¥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:

  • 许多等效表示仍然编码了某种程度的意图,最好将其保留在声明文件中。

    ¥Many equivalent representations still encode some level of intent that is better to preserve in the declaration file

  • 生成类型的全新表示可能成本较高,因此最好避免。

    ¥Producing a fresh representation of a type can be somewhat expensive, so avoiding is better

  • 用户编写的类型通常比生成的类型表示更短。

    ¥User-written types are usually shorter than generated type representations

在 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 就无需为对象字面量中的每个属性重新计算。。执行此优化可将 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 中使用 TypeScript npm 包中的命名导入和 ECMAScript 模块。

¥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

TypeScript 的 API 公开了一个名为 transpileModule 的函数。它旨在简化 TypeScript 代码的单个文件的编译。由于 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

    ¥prepend in project references

  • 隐式特定于操作系统的 newLine

    ¥implicitly OS-specific 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 上找到,包含如何最佳地调整代码库的建议。

¥More information around these deprecation plans is available on GitHub, which contains suggestions in how to best adapt your codebase.

lib.d.ts 变更

¥lib.d.ts Changes

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

¥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() { }
}
}
}

更多关于此变更的信息,请点击此处

¥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;

由于一个 bug,此逻辑不适用于内置类型 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.

简化引用指令声明的 Emit

¥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.

通过 experimentation,我们发现几乎所有 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 中大大简化声明 emit 中的引用指令。更一致的策略将帮助库作者和使用者更好地控制其声明文件。

¥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 的项目中,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.