using 声明和显式资源管理
🌐 using Declarations and Explicit Resource Management
TypeScript 5.2 为即将到来的 ECMAScript 显式资源管理 功能添加了支持。让我们探讨一下其中的一些动机,并了解该功能为我们带来了什么。
🌐 TypeScript 5.2 adds support for the upcoming Explicit Resource Management feature in ECMAScript. Let’s explore some of the motivations and understand what the feature brings us.
在创建对象后,通常需要进行某种“清理”。例如,你可能需要关闭网络连接、删除临时文件,或者只是释放一些内存。
🌐 It’s common to need to do some sort of “clean-up” after creating an object. For example, you might need to close network connections, delete temporary files, or just free up some memory.
假设有一个函数创建一个临时文件,对其进行读写操作以进行各种操作,然后关闭并删除该文件。
🌐 Let’s imagine a function that creates a temporary file, reads and writes to it for various operations, and then closes and deletes it.
tsimport * as fs from "fs";export function doSomeWork() {const path = ".some_temp_file";const file = fs.openSync(path, "w+");// use file...// Close the file and delete it.fs.closeSync(file);fs.unlinkSync(path);}
这没问题,但如果我们需要提前退出会发生什么?
🌐 This is fine, but what happens if we need to perform an early exit?
tsexport function doSomeWork() {const path = ".some_temp_file";const file = fs.openSync(path, "w+");// use file...if (someCondition()) {// do some more work...// Close the file and delete it.fs.closeSync(file);fs.unlinkSync(path);return;}// Close the file and delete it.fs.closeSync(file);fs.unlinkSync(path);}
我们开始看到一些清理操作的重复,这很容易被遗忘。我们也不能保证在出现错误时关闭并删除文件。可以通过将所有这些封装在一个 try/finally 块中来解决这个问题。
🌐 We’re starting to see some duplication of clean-up which can be easy to forget.
We’re also not guaranteed to close and delete the file if an error gets thrown.
This could be solved by wrapping this all in a try/finally block.
tsexport function doSomeWork() {const path = ".some_temp_file";const file = fs.openSync(path, "w+");try {// use file...if (someCondition()) {// do some more work...return;}}finally {// Close the file and delete it.fs.closeSync(file);fs.unlinkSync(path);}}
虽然这更健壮,但它给我们的代码增加了相当多的“噪音”。
如果我们开始在 finally 块中添加更多清理逻辑,还可能遇到其他潜在问题——例如,异常可能会阻止其他资源被释放。
这正是 显式资源管理 提案想要解决的问题。
该提案的核心理念是将资源释放——我们试图处理的清理工作——作为 JavaScript 的一个一级概念来支持。
🌐 While this is more robust, it’s added quite a bit of “noise” to our code.
There are also other foot-guns we can run into if we start adding more clean-up logic to our finally block — for example, exceptions preventing other resources from being disposed.
This is what the explicit resource management proposal aims to solve.
The key idea of the proposal is to support resource disposal — this clean-up work we’re trying to deal with — as a first class idea in JavaScript.
这首先通过添加一个名为 Symbol.dispose 的新内置 symbol 开始,我们可以创建具有由 Symbol.dispose 命名的方法的对象。为了方便,TypeScript 定义了一个新的全局类型 Disposable 来描述这些对象。
🌐 This starts by adding a new built-in symbol called Symbol.dispose, and we can create objects with methods named by Symbol.dispose.
For convenience, TypeScript defines a new global type called Disposable which describes these.
tsclass TempFile implements Disposable {#path: string;#handle: number;constructor(path: string) {this.#path = path;this.#handle = fs.openSync(path, "w+");}// other methods[Symbol.dispose]() {// Close the file and delete it.fs.closeSync(this.#handle);fs.unlinkSync(this.#path);}}
稍后我们可以调用这些方法。
🌐 Later on we can call those methods.
tsexport function doSomeWork() {const file = new TempFile(".some_temp_file");try {// ...}finally {file[Symbol.dispose]();}}
将清理逻辑移到 TempFile 本身并没有带来太多好处;我们基本上只是把 finally 块中的所有清理工作移到了一个方法中,而这一直都是可能的。但拥有一个众所周知的“名称”来表示这个方法意味着 JavaScript 可以在其基础上构建其他功能。
🌐 Moving the clean-up logic to TempFile itself doesn’t buy us much;
we’ve basically just moved all the clean-up work from the finally block into a method, and that’s always been possible.
But having a well-known “name” for this method means that JavaScript can build other features on top of it.
这就引出了本特性的第一个亮点:using 声明!
using 是一个新关键字,它允许我们声明新的固定绑定,有点像 const。
关键区别在于,用 using 声明的变量会在作用域结束时调用它们的 Symbol.dispose 方法!
🌐 That brings us to the first star of the feature: using declarations!
using is a new keyword that lets us declare new fixed bindings, kind of like const.
The key difference is that variables declared with using get their Symbol.dispose method called at the end of the scope!
所以我们可以简单地像这样编写代码:
🌐 So we could simply have written our code like this:
tsexport function doSomeWork() {using file = new TempFile(".some_temp_file");// use file...if (someCondition()) {// do some more work...return;}}
看看吧——没有 try/finally 块!至少,我们没看到有。功能上,这正是 using 声明为我们做的事情,但我们不必处理它。
🌐 Check it out — no try/finally blocks!
At least, none that we see.
Functionally, that’s exactly what using declarations will do for us, but we don’t have to deal with that.
你可能熟悉 C# 中的 using 声明、Python 中的 with 语句,或 Java 中的 try-with-resource 声明。
这些都类似于 JavaScript 中新的 using 关键字,并提供了一种在作用域结束时对对象进行“清理”的类似显式方式。
🌐 You might be familiar with using declarations in C#, with statements in Python, or try-with-resource declarations in Java.
These are all similar to JavaScript’s new using keyword, and provide a similar explicit way to perform a “tear-down” of an object at the end of a scope.
using 声明会在其所在作用域的最后,或者在像 return 或 throw 之类的“提前返回”之前进行清理。它们还会以后进先出(栈)的顺序进行释放。
tsfunction loggy(id: string): Disposable {console.log(`Creating ${id}`);return {[Symbol.dispose]() {console.log(`Disposing ${id}`);}}}function func() {using a = loggy("a");using b = loggy("b");{using c = loggy("c");using d = loggy("d");}using e = loggy("e");return;// Unreachable.// Never created, never disposed.using f = loggy("f");}func();// Creating a// Creating b// Creating c// Creating d// Disposing d// Disposing c// Creating e// Disposing e// Disposing b// Disposing a
using 声明应该能够抵御异常;如果抛出错误,在处理后会重新抛出。另一方面,你的函数体可能按预期执行,但 Symbol.dispose 可能会抛出异常。在这种情况下,该异常也会被重新抛出。
但是如果在释放之前和释放过程中逻辑都抛出错误会发生什么呢?
对于这些情况,引入了 SuppressedError 作为 Error 的一个新子类型。
它具有一个 suppressed 属性用于保存最后抛出的错误,以及一个 error 属性用于保存最近抛出的错误。
🌐 But what happens if both the logic before and during disposal throws an error?
For those cases, SuppressedError has been introduced as a new subtype of Error.
It features a suppressed property that holds the last-thrown error, and an error property for the most-recently thrown error.
tsclass ErrorA extends Error {name = "ErrorA";}class ErrorB extends Error {name = "ErrorB";}function throwy(id: string) {return {[Symbol.dispose]() {throw new ErrorA(`Error from ${id}`);}};}function func() {using a = throwy("a");throw new ErrorB("oops!")}try {func();}catch (e: any) {console.log(e.name); // SuppressedErrorconsole.log(e.message); // An error was suppressed during disposal.console.log(e.error.name); // ErrorAconsole.log(e.error.message); // Error from aconsole.log(e.suppressed.name); // ErrorBconsole.log(e.suppressed.message); // oops!}
你可能已经注意到,我们在这些示例中使用的是同步方法。然而,许多资源的释放涉及异步操作,我们需要等待这些操作完成后,才能继续执行其他代码。
🌐 You might have noticed that we’re using synchronous methods in these examples. However, lots of resource disposal involves asynchronous operations, and we need to wait for those to complete before we continue running any other code.
这就是为什么会有一个新的 Symbol.asyncDispose,它将我们引向节目中的下一个明星——await using 声明。
它们类似于 using 声明,但关键在于它们会查找谁的处理必须被 await。
它们使用一个名为 Symbol.asyncDispose 的不同方法,尽管它们也可以对任何具有 Symbol.dispose 的对象操作。
为了方便起见,TypeScript 还引入了一个全局类型 AsyncDisposable,它描述了任何具有异步处理方法的对象。
🌐 That’s why there is also a new Symbol.asyncDispose, and it brings us to the next star of the show — await using declarations.
These are similar to using declarations, but the key is that they look up whose disposal must be awaited.
They use a different method named by Symbol.asyncDispose, though they can operate on anything with a Symbol.dispose as well.
For convenience, TypeScript also introduces a global type called AsyncDisposable that describes any object with an asynchronous dispose method.
tsasync function doWork() {// Do fake work for half a second.await new Promise(resolve => setTimeout(resolve, 500));}function loggy(id: string): AsyncDisposable {console.log(`Constructing ${id}`);return {async [Symbol.asyncDispose]() {console.log(`Disposing (async) ${id}`);await doWork();},}}async function func() {await using a = loggy("a");await using b = loggy("b");{await using c = loggy("c");await using d = loggy("d");}await using e = loggy("e");return;// Unreachable.// Never created, never disposed.await using f = loggy("f");}func();// Constructing a// Constructing b// Constructing c// Constructing d// Disposing (async) d// Disposing (async) c// Constructing e// Disposing (async) e// Disposing (async) b// Disposing (async) a
如果你希望其他人能够一致地执行拆卸逻辑,那么用 Disposable 和 AsyncDisposable 来定义类型会让你的代码更容易使用。事实上,现有的许多类型在实际中都有 dispose() 或 close() 方法。例如,Visual Studio Code 的 API 甚至定义了 它们自己的 Disposable 接口。浏览器中的 API 以及 Node.js、Deno 和 Bun 等运行时的 API,也可能选择对已经有清理方法的对象(如文件句柄、连接等)使用 Symbol.dispose 和 Symbol.asyncDispose。
🌐 Defining types in terms of Disposable and AsyncDisposable can make your code much easier to work with if you expect others to do tear-down logic consistently.
In fact, lots of existing types exist in the wild which have a dispose() or close() method.
For example, the Visual Studio Code APIs even define their own Disposable interface.
APIs in the browser and in runtimes like Node.js, Deno, and Bun might also choose to use Symbol.dispose and Symbol.asyncDispose for objects which already have clean-up methods, like file handles, connections, and more.
现在也许这对库来说听起来很棒,但对于你的场景来说可能有点过于笨重。如果你要做大量的临时清理,创建一个新类型可能会引入过多的抽象,并带来关于最佳实践的疑问。例如,再来看我们的 TempFile 示例。
🌐 Now maybe this all sounds great for libraries, but a little bit heavy-weight for your scenarios.
If you’re doing a lot of ad-hoc clean-up, creating a new type might introduce a lot of over-abstraction and questions about best-practices.
For example, take our TempFile example again.
tsclass TempFile implements Disposable {#path: string;#handle: number;constructor(path: string) {this.#path = path;this.#handle = fs.openSync(path, "w+");}// other methods[Symbol.dispose]() {// Close the file and delete it.fs.closeSync(this.#handle);fs.unlinkSync(this.#path);}}export function doSomeWork() {using file = new TempFile(".some_temp_file");// use file...if (someCondition()) {// do some more work...return;}}
我们所想做的只是记得调用两个函数——但这是写这段代码的最佳方式吗?
我们应该在构造函数中调用 openSync,创建一个 open() 方法,还是自己传入句柄?
我们是否应该为我们需要执行的每一个操作都暴露一个方法,还是干脆把属性设为公共的?
🌐 All we wanted was to remember to call two functions — but was this the best way to write it?
Should we be calling openSync in the constructor, create an open() method, or pass in the handle ourselves?
Should we expose a method for every possible operation we need to perform, or should we just make the properties public?
这就把我们带到了本特性的最后几个明星:DisposableStack 和 AsyncDisposableStack。 这些对象对于一次性的清理以及任意数量的清理都很有用。 DisposableStack 是一个拥有多个方法来跟踪 Disposable 对象的对象,并且可以给它们分配函数来进行任意的清理工作。 我们也可以将它们赋值给 using 变量,因为——你看——它们也是 Disposable! 所以,这就是我们原本可以写原始示例的方式。
🌐 That brings us to the final stars of the feature: DisposableStack and AsyncDisposableStack.
These objects are useful for doing both one-off clean-up, along with arbitrary amounts of cleanup.
A DisposableStack is an object that has several methods for keeping track of Disposable objects, and can be given functions for doing arbitrary clean-up work.
We can also assign them to using variables because — get this — they’re also Disposable!
So here’s how we could’ve written the original example.
tsfunction doSomeWork() {const path = ".some_temp_file";const file = fs.openSync(path, "w+");using cleanup = new DisposableStack();cleanup.defer(() => {fs.closeSync(file);fs.unlinkSync(path);});// use file...if (someCondition()) {// do some more work...return;}// ...}
在这里,defer() 方法只是接收一个回调函数,而该回调将在 cleanup 被释放后运行。通常,defer(以及其他类似 use 和 adopt 的 DisposableStack 方法)应该在创建资源后立即调用。顾名思义,DisposableStack 会按照后进先出顺序处理它所跟踪的所有内容,所以在创建一个值后立即 defer 可以帮助避免奇怪的依赖问题。AsyncDisposableStack 的工作原理类似,但它可以跟踪 async 函数和 AsyncDisposable,并且它本身是一个 AsyncDisposable.。
🌐 Here, the defer() method just takes a callback, and that callback will be run once cleanup is disposed of.
Typically, defer (and other DisposableStack methods like use and adopt)
should be called immediately after creating a resource.
As the name suggests, DisposableStack disposes of everything it keeps track of like a stack, in a first-in-last-out order, so defering immediately after creating a value helps avoid odd dependency issues.
AsyncDisposableStack works similarly, but can keep track of async functions and AsyncDisposables, and is itself an AsyncDisposable.
defer 方法在许多方面类似于 Go、Swift、Zig、Odin 等语言中的 defer 关键字,其约定也应该是相似的。
🌐 The defer method is similar in many ways to the defer keyword in Go, Swift, Zig, Odin, and others, where the conventions should be similar.
由于此功能非常新,大多数运行时不会原生支持它。要使用它,你需要以下运行时填充:
🌐 Because this feature is so recent, most runtimes will not support it natively. To use it, you will need runtime polyfills for the following:
Symbol.disposeSymbol.asyncDisposeDisposableStackAsyncDisposableStackSuppressedError
然而,如果你只对 using 和 await using 感兴趣,你应该只需要为内置的 symbol 添加 polyfill 就足够了。像下面这样简单的做法在大多数情况下应该都能奏效:
🌐 However, if all you’re interested in is using and await using, you should be able to get away with only polyfilling the built-in symbols.
Something as simple as the following should work for most cases:
tsSymbol.dispose ??= Symbol("Symbol.dispose");Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
你还需要将你的编译 target 设置为 es2022 或更低,并将你的 lib 设置配置为包含 "esnext" 或 "esnext.disposable"。
🌐 You will also need to set your compilation target to es2022 or below, and configure your lib setting to either include "esnext" or "esnext.disposable".
json{"compilerOptions": {"target": "es2022","lib": ["es2022", "esnext.disposable", "dom"]}}
有关此功能的更多信息,请查看 GitHub 上的相关工作!
🌐 For more information on this feature, take a look at the work on GitHub!
装饰器元数据
🌐 Decorator Metadata
TypeScript 5.2 实现了一个即将到来的 ECMAScript 特性,称为装饰器元数据。
🌐 TypeScript 5.2 implements an upcoming ECMAScript feature called decorator metadata.
此功能的核心思想是使装饰器能够轻松地在其所用或所包含的任何类上创建和使用元数据。
🌐 The key idea of this feature is to make it easy for decorators to create and consume metadata on any class they’re used on or within.
每当使用装饰器函数时,它们现在可以访问上下文对象上的新 metadata 属性。
metadata 属性只是保存一个简单的对象。
由于 JavaScript 允许我们任意添加属性,它可以被用作由每个装饰器更新的字典。
或者,由于每个 metadata 对象对于类的每个被装饰部分都是相同的,它可以被用作 Map 的键。
在类上的所有装饰器运行完毕后,可以通过 Symbol.metadata 访问该对象。
🌐 Whenever decorator functions are used, they now have access to a new metadata property on their context object.
The metadata property just holds a simple object.
Since JavaScript lets us add properties arbitrarily, it can be used as a dictionary that is updated by each decorator.
Alternatively, since every metadata object will be identical for each decorated portion of a class, it can be used as a key into a Map.
After all decorators on or in a class get run, that object can be accessed on the class via Symbol.metadata.
tsinterface Context {name: string;metadata: Record<PropertyKey, unknown>;}function setMetadata(_target: any, context: Context) {context.metadata[context.name] = true;}class SomeClass {@setMetadatafoo = 123;@setMetadataaccessor bar = "hello!";@setMetadatabaz() { }}const ourMetadata = SomeClass[Symbol.metadata];console.log(JSON.stringify(ourMetadata));// { "bar": true, "baz": true, "foo": true }
这在很多不同的场景下都可能很有用。元数据可能会用于很多用途,比如调试、序列化,或者使用装饰器进行依赖注入。由于元数据对象是为每个被装饰的类创建的,框架可以将它们私下用作 Map 或 WeakMap 的键,或者根据需要附加属性。
🌐 This can be useful in a number of different scenarios.
Metadata could possibly be attached for lots of uses like debugging, serialization, or performing dependency injection with decorators.
Since metadata objects are created per decorated class, frameworks can either privately use them as keys into a Map or WeakMap, or tack properties on as necessary.
例如,假设我们想使用装饰器来跟踪在使用 JSON.stringify 时哪些属性和访问器是可序列化的,如下所示:
🌐 For example, let’s say we wanted to use decorators to keep track of which properties and accessors are serializable when using JSON.stringify like so:
tsimport { serialize, jsonify } from "./serializer";class Person {firstName: string;lastName: string;@serializeage: number@serializeget fullName() {return `${this.firstName} ${this.lastName}`;}toJSON() {return jsonify(this)}constructor(firstName: string, lastName: string, age: number) {// ...}}
这里的意图是只有 age 和 fullName 应该被序列化,因为它们被标记了 @serialize 装饰器。我们为此定义了一个 toJSON 方法,但它只是调用 jsonify,而 jsonify 使用的是 @serialize 创建的元数据。
🌐 Here, the intent is that only age and fullName should be serialized because they are marked with the @serialize decorator.
We define a toJSON method for this purpose, but it just calls out to jsonify which uses the metadata that @serialize created.
以下是模块 ./serialize.ts 可能的定义示例:
🌐 Here’s an example of how the module ./serialize.ts might be defined:
tsconst serializables = Symbol();type Context =| ClassAccessorDecoratorContext| ClassGetterDecoratorContext| ClassFieldDecoratorContext;export function serialize(_target: any, context: Context): void {if (context.static || context.private) {throw new Error("Can only serialize public instance members.")}if (typeof context.name === "symbol") {throw new Error("Cannot serialize symbol-named properties.");}const propNames =(context.metadata[serializables] as string[] | undefined) ??= [];propNames.push(context.name);}export function jsonify(instance: object): string {const metadata = instance.constructor[Symbol.metadata];const propNames = metadata?.[serializables] as string[] | undefined;if (!propNames) {throw new Error("No members marked with @serialize.");}const pairStrings = propNames.map(key => {const strKey = JSON.stringify(key);const strValue = JSON.stringify((instance as any)[key]);return `${strKey}: ${strValue}`;});return `{ ${pairStrings.join(", ")} }`;}
该模块有一个本地的 symbol,称为 serializables,用于存储和检索标记为 @serializable 的属性名称。它在每次调用 @serializable 时将这些属性名称的列表存储在元数据上。当调用 jsonify 时,会从元数据中获取属性列表,并用于从实例中检索实际的值,最终对这些名称和值进行序列化。
🌐 This module has a local symbol called serializables to store and retrieve the names of properties marked @serializable.
It stores a list of these property names on the metadata on each invocation of @serializable.
When jsonify is called, the list of properties is fetched off of the metadata and used to retrieve the actual values from the instance, eventually serializing those names and values.
使用 symbol 从技术上讲会让其他人可以访问这些数据。另一种方法可能是使用 WeakMap,将元数据对象作为键。这样可以保持数据私密,并且在这种情况下实际上使用的类型断言更少,但其他方面是类似的。
🌐 Using a symbol technically makes this data accessible to others.
An alternative might be to use a WeakMap using the metadata object as a key.
This keeps data private and happens to use fewer type assertions in this case, but is otherwise similar.
tsconst serializables = new WeakMap<object, string[]>();type Context =| ClassAccessorDecoratorContext| ClassGetterDecoratorContext| ClassFieldDecoratorContext;export function serialize(_target: any, context: Context): void {if (context.static || context.private) {throw new Error("Can only serialize public instance members.")}if (typeof context.name !== "string") {throw new Error("Can only serialize string properties.");}let propNames = serializables.get(context.metadata);if (propNames === undefined) {serializables.set(context.metadata, propNames = []);}propNames.push(context.name);}export function jsonify(instance: object): string {const metadata = instance.constructor[Symbol.metadata];const propNames = metadata && serializables.get(metadata);if (!propNames) {throw new Error("No members marked with @serialize.");}const pairStrings = propNames.map(key => {const strKey = JSON.stringify(key);const strValue = JSON.stringify((instance as any)[key]);return `${strKey}: ${strValue}`;});return `{ ${pairStrings.join(", ")} }`;}
需要注意的是,这些实现并没有处理子类化和继承。这部分留给你自己去练习(你可能会发现,在文件的某个版本中,这比另一个版本更容易!)。
🌐 As a note, these implementations don’t handle subclassing and inheritance. That’s left as an exercise to you (and you might find that it is easier in one version of the file than the other!).
由于此功能仍然较新,大多数运行时环境不会原生支持它。
要使用它,你需要为 Symbol.metadata 提供一个垫片(polyfill)。
像下面这样简单的示例在大多数情况下都应该可以使用:
🌐 Because this feature is still fresh, most runtimes will not support it natively.
To use it, you will need a polyfill for Symbol.metadata.
Something as simple as the following should work for most cases:
tsSymbol.metadata ??= Symbol("Symbol.metadata");
你还需要将你的编译 target 设置为 es2022 或更低,并将你的 lib 设置配置为包含 "esnext" 或 "esnext.decorators"。
🌐 You will also need to set your compilation target to es2022 or below, and configure your lib setting to either include "esnext" or "esnext.decorators".
json{"compilerOptions": {"target": "es2022","lib": ["es2022", "esnext.decorators", "dom"]}}
我们想感谢 Oleksandr Tarasiuk 为 TypeScript 5.2 贡献了 装饰器元数据的实现!
🌐 We’d like to thank Oleksandr Tarasiuk for contributing the implementation of decorator metadata for TypeScript 5.2!
命名元组和匿名元组元素
🌐 Named and Anonymous Tuple Elements
元组类型支持每个元素的可选标签或名称。
🌐 Tuple types have supported optional labels or names for each element.
tstype Pair<T> = [first: T, second: T];
这些标签不会改变你使用它们执行的操作 - 它们仅仅是为了提高可读性和工具性。
🌐 These labels don’t change what you’re allowed to do with them — they’re solely to help with readability and tooling.
然而,TypeScript 之前有一条规则,元组不能在带标签和不带标签的元素之间混合使用。换句话说,元组中的元素要么一个都不带标签,要么所有元素都必须带标签。
🌐 However, TypeScript previously had a rule that tuples could not mix and match between labeled and unlabeled elements. In other words, either no element could have a label in a tuple, or all elements needed one.
ts// ✅ fine - no labelstype Pair1<T> = [T, T];// ✅ fine - all fully labeledtype Pair2<T> = [first: T, second: T];// ❌ previously an errortype Pair3<T> = [first: T, T];// ~// Tuple members must all have names// or all not have names.
对于那些必须添加像 rest 或 tail 这样的标签的剩余元素来说,这可能会很烦人。
🌐 This could be annoying for rest elements where we’d be forced to just add a label like rest or tail.
ts// ❌ previously an errortype TwoOrMore_A<T> = [first: T, second: T, ...T[]];// ~~~~~~// Tuple members must all have names// or all not have names.// ✅type TwoOrMore_B<T> = [first: T, second: T, rest: ...T[]];
这也意味着此限制必须在类型系统内部强制执行,这意味着 TypeScript 会丢失标签。
🌐 It also meant that this restriction had to be enforced internally in the type system, meaning TypeScript would lose labels.
tstype HasLabels = [a: string, b: string];type HasNoLabels = [number, number];type Merged = [...HasNoLabels, ...HasLabels];// ^ [number, number, string, string]//// 'a' and 'b' were lost in 'Merged'
在 TypeScript 5.2 中,对元组标签的全有或全无限制已被取消。语言现在也可以在展开到无标签元组时保留标签。
🌐 In TypeScript 5.2, the all-or-nothing restriction on tuple labels has been lifted. The language can now also preserve labels when spreading into an unlabeled tuple.
我们想向 Josh Goldberg 和 Mateusz Burzyński 表示感谢,他们 合作解除这一限制。
🌐 We’d like to extend our thanks to Josh Goldberg and Mateusz Burzyński who collaborated to lift this restriction.
更轻松地使用数组联合的方法
🌐 Easier Method Usage for Unions of Arrays
在之前的 TypeScript 版本中,在数组联合上调用方法可能会很麻烦。
🌐 In previous versions of TypeScript, calling a method on a union of arrays could end in pain.
tsdeclare let array: string[] | number[];array.filter(x => !!x);// ~~~~~~ error!// This expression is not callable.// Each member of the union type '...' has signatures,// but none of those signatures are compatible// with each other.
在这个例子中,TypeScript 会尝试查看每个 filter 版本是否在 string[] 和 number[] 之间兼容。没有统一的策略,TypeScript 就束手无策,说“我搞不定”。
🌐 In this example, TypeScript would try to see if each version of filter is compatible across string[] and number[].
Without a coherent strategy, TypeScript threw its hands in the air and said “I can’t make it work”.
在 TypeScript 5.2 中,在放弃这些情况之前,数组联合被视为一种特殊情况。会根据每个成员的元素类型构建新的数组类型,然后在该类型上调用方法。
🌐 In TypeScript 5.2, before giving up in these cases, unions of arrays are treated as a special case. A new array type is constructed out of each member’s element type, and then the method is invoked on that.
以上例子中,string[] | number[] 被转换成 (string | number)[](或 Array<string | number>),并且在该类型上调用 filter。有一点需要注意的是,filter 会产生 Array<string | number> 而不是 string[] | number[];但对于新生成的值来说,出现问题的风险较小。
🌐 Taking the above example, string[] | number[] is transformed into (string | number)[] (or Array<string | number>), and filter is invoked on that type.
There is a slight caveat which is that filter will produce an Array<string | number> instead of a string[] | number[];
but for a freshly produced value there is less risk of something “going wrong”.
这意味着像 filter、find、some、every 和 reduce 这样的许多方法现在都应该可以在数组联合类型上调用,而在之前的情况下是不可以的。
🌐 This means lots of methods like filter, find, some, every, and reduce should all be invokable on unions of arrays in cases where they were not previously.
🌐 You can read up more details on the implementing pull request.
仅类型导入路径和 TypeScript 实现文件扩展名
🌐 Type-Only Import Paths with TypeScript Implementation File Extensions
TypeScript 现在允许在仅类型导入路径中包含声明文件和实现文件的扩展名,无论是否启用了 allowImportingTsExtensions。
🌐 TypeScript now allows both declaration and implementation file extensions to be included in type-only import paths, regardless of whether allowImportingTsExtensions is enabled.
这意味着你现在可以编写使用 .ts、.mts、.cts 和 .tsx 文件扩展名的 import type 语句。
🌐 This means that you can now write import type statements that use .ts, .mts, .cts, and .tsx file extensions.
tsimport type { JustAType } from "./justTypes.ts";export function f(param: JustAType) {// ...}
这也意味着 import() 类型可以在 TypeScript 和带有 JSDoc 的 JavaScript 中使用,并且可以使用这些文件扩展名。
🌐 It also means that import() types, which can be used in both TypeScript and JavaScript with JSDoc, can use those file extensions.
js/*** @param {import("./justTypes.ts").JustAType} param*/export function f(param) {// ...}
欲了解更多信息,请点击此处查看更改。
🌐 For more information, see the change here.
对象成员的逗号补全
🌐 Comma Completions for Object Members
在向对象添加新属性时,很容易忘记添加逗号。以前,如果你忘记了逗号并请求自动补齐,TypeScript 会混乱地给出不相关的补全结果。
🌐 It can be easy to forget to add a comma when adding a new property to an object. Previously, if you forgot a comma and requested auto-completion, TypeScript would confusingly give poor unrelated completion results.
TypeScript 5.2 现在可以在你忘记逗号时优雅地提供对象成员补全。但为了避免抛出语法错误,它也会自动插入缺失的逗号。
🌐 TypeScript 5.2 now gracefully provides object member completions when you’re missing a comma. But to just skip past hitting you with a syntax error, it will also auto-insert the missing comma.

欲了解更多信息,请在此查看实现。
🌐 For more information, see the implementation here.
内联变量重构
🌐 Inline Variable Refactoring
TypeScript 5.2 现在进行了重构,可以将变量的内容内联到所有使用位置。
🌐 TypeScript 5.2 now has a refactoring to inline the contents of a variable to all usage sites.
。
使用“内联变量”重构将会删除该变量,并将所有变量的使用替换为其初始化值。请注意,这可能会导致该初始化值的副作用在不同的时间运行,并且会运行与变量使用次数相同的次数。
🌐 Using the “inline variable” refactoring will eliminate the variable and replace all the variable’s usages with its initializer. Note that this may cause that initializer’s side-effects to run at a different time, and as many times as the variable has been used.
欲了解更多详情,请参阅实现该功能的拉取请求。
🌐 For more details, see the implementing pull request.
优化持续类型兼容性检查
🌐 Optimized Checks for Ongoing Type Compatibility
由于 TypeScript 是一种结构类型系统,类型有时需要按成员逐一比较;然而,递归类型在这里会带来一些问题。例如:
🌐 Because TypeScript is a structural type system, types occasionally need to be compared in a member-wise fashion; however, recursive types add some issues here. For example:
tsinterface A {value: A;other: string;}interface B {value: B;other: number;}
在检查类型 A 是否与类型 B 兼容时,TypeScript 最终会检查 A 和 B 中的 value 类型是否各自兼容。此时,类型系统需要停止进一步检查,并继续检查其他成员。为此,类型系统必须追踪任何两个类型何时已经建立了关联。
🌐 When checking whether the type A is compatible with the type B, TypeScript will end up checking whether the types of value in A and B are respectively compatible.
At this point, the type system needs to stop checking any further and proceed to check other members.
To do this, the type system has to track when any two types are already being related.
之前 TypeScript 已经维护了一堆类型对的栈,并通过遍历它来确定这些类型是否相关。当这个栈比较浅时,这没问题;但是当栈不浅时,那,呃,就是一个问题。
🌐 Previously TypeScript already kept a stack of type pairs, and iterated through that to determine whether those types are being related. When this stack is shallow that’s not a problem; but when the stack isn’t shallow, that, uh, is a problem.
在 TypeScript 5.3 中,一个简单的 Set 有助于跟踪这些信息。这减少了在使用 drizzle 库的报告测试用例上花费的时间超过 33%!
🌐 In TypeScript 5.3, a simple Set helps track this information.
This reduced the time spent on a reported test case that used the drizzle library by over 33%!
Benchmark 1: oldTime (mean ± σ): 3.115 s ± 0.067 s [User: 4.403 s, System: 0.124 s]Range (min … max): 3.018 s … 3.196 s 10 runsBenchmark 2: newTime (mean ± σ): 2.072 s ± 0.050 s [User: 3.355 s, System: 0.135 s]Range (min … max): 1.985 s … 2.150 s 10 runsSummary'new' ran1.50 ± 0.05 times faster than 'old'
重大变更和正确性修复
🌐 Breaking Changes and Correctness Fixes
TypeScript 力求不无故引入中断;然而,有时我们必须进行修复和改进,以便代码能够被更好地分析。
🌐 TypeScript strives not to unnecessarily introduce breaks; however, occasionally we must make corrections and improvements so that code can be better-analyzed.
lib.d.ts 变更
🌐 lib.d.ts Changes
为 DOM 生成的类型可能会影响你的代码库。有关更多信息,请参阅 TypeScript 5.2 的 DOM 更新。
🌐 Types generated for the DOM may have an impact on your codebase. For more information, see the DOM updates for TypeScript 5.2.
labeledElementDeclarations 可能包含 undefined 个元素
🌐 labeledElementDeclarations May Hold undefined Elements
为了支持标记和未标记元素的混合,TypeScript 的 API 已略有更改。 TupleType 的 labeledElementDeclarations 属性可能在每个未标记元素的位置包含 undefined。
🌐 In order to support a mixture of labeled and unlabeled elements, TypeScript’s API has changed slightly.
The labeledElementDeclarations property of TupleType may hold undefined for at each position where an element is unlabeled.
diffinterface TupleType {- labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration)[];+ labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration | undefined)[];}
module 和 moduleResolution 在最新的 Node.js 设置下必须匹配
🌐 module and moduleResolution Must Match Under Recent Node.js settings
--module 和 --moduleResolution 选项各自支持 node16 和 nodenext 设置。这些实际上是“现代 Node.js”设置,应在任何近期的 Node.js 项目中使用。我们发现,当这两个选项在是否使用与 Node.js 相关的设置上不一致时,项目实际上就是配置错误的。
🌐 The --module and --moduleResolution options each support a node16 and nodenext setting.
These are effectively “modern Node.js” settings that should be used on any recent Node.js project.
What we’ve found is that when these two options don’t agree on whether they are using Node.js-related settings, projects are effectively misconfigured.
在 TypeScript 5.2 中,当 ‘node16’ 或 ‘nodenext’ 分别设置 ‘—module’ 和 ‘—moduleResolution’ 时,TypeScript 现在要求另一方设置与 Node.js 相关。 如果设置不一致,你很可能会收到类似以下几种的错误提示
🌐 In TypeScript 5.2, when using node16 or nodenext for either of the --module and --moduleResolution options, TypeScript now requires the other to have a similar Node.js-related setting.
In cases where the settings diverge, you’ll likely get an error message like either
Option 'moduleResolution' must be set to 'NodeNext' (or left unspecified) when option 'module' is set to 'NodeNext'.
or
Option 'module' must be set to 'Node16' when option 'moduleResolution' is set to 'Node16'.
例如,--module esnext --moduleResolution node16 会被拒绝——但你可能更好直接使用 --module nodenext,或者 --module esnext --moduleResolution bundler。
🌐 So for example --module esnext --moduleResolution node16 will be rejected — but you may be better off just using --module nodenext alone, or --module esnext --moduleResolution bundler.
欲了解更多信息,请点击此处查看更改。
🌐 For more information, see the change here.
合并符号的一致性导出检查
🌐 Consistent Export Checking for Merged Symbols
当两个声明合并时,它们必须就是否都被导出达成一致。由于一个漏洞,TypeScript 在一些全局上下文中,如声明文件或 declare module 块中,漏掉了特定情况。例如,对于以下情况,它不会报错:replaceInFile 被声明为一个导出函数,并且另一个作为未导出的命名空间。
🌐 When two declarations merge, they must agree on whether they are both exported.
Due to a bug, TypeScript missed specific cases in ambient contexts, like in declaration files or declare module blocks.
For example, it would not issue an error on a case like the following, where replaceInFile is declared once as an exported function, and one as an un-exported namespace.
tsdeclare module 'replace-in-file' {export function replaceInFile(config: unknown): Promise<unknown[]>;export {};namespace replaceInFile {export function sync(config: unknown): unknown[];}}
在一个环境模块中,添加 export { ... } 或类似 export default ... 的结构会隐式地改变是否自动导出所有声明。TypeScript 现在对这些不幸令人困惑的语义有了更一致的识别,并且会对所有 replaceInFile 的声明需要在修饰符上保持一致这一事实发出错误,并将提示以下错误:
🌐 In an ambient module, adding an export { ... } or a similar construct like export default ... implicitly changes whether all declarations are automatically exported.
TypeScript now recognizes these unfortunately confusing semantics more consistently, and issues an error on the fact that all declarations of replaceInFile need to agree in their modifiers, and will issue the following error:
Individual declarations in merged declaration 'replaceInFile' must be all exported or all local.
欲了解更多信息,请点击此处查看更改。
🌐 For more information, see the change here.