[翻译] Announcing TypeScript 5.2 Beta
原文 https://devblogs.microsoft.com/typescript/announcing-typescript-5-2-beta/
如何安装
npm install -D typescript@beta
TypeScript 5.2 新特性一览
using
Declarations and Explicit Resource Management- Decorator Metadata
- Named and Anonymous Tuple Elements
- Easier Method Usage for Unions of Arrays
- Comma Completions for Object Members
- Inline Variable Refactoring
- Breaking Changes and Correctness Fixes
using
声明和显式资源管理
TypeScript 5.2 添加了对即将到来的ECMAScript特性 - 显式资源管理的支持. 我们来探索一下添加该特性的一些动机, 以便于理解它给我们带来了什么新功能.
在创建一个对象, 并使用完后要执行某种清理操作是一种常见的开发场景. 例如, 你可能需要关闭网络链接, 删除临时文件, 或者只是释放某些内存.
我们想象一个函数, 它会创建一个临时文件, 然后使用各种操作来读写该文件, 最后关闭和删除该临时文件.
import * 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);
}
看上去挺好, 但是如果我们需要执行一个提前退出会发生什么?
export 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
代码块中.
export 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的一等公民"类"中支持资源释放, 也就是我们想要处理的清理工作.
首先添加一个新的内置symbol
, 称之为Symbol.dispose
, 然后创建含有成员方法名为Symbol.dispose
的对象. 方便起见, TypeScript定义了一个新的全局类型, 称之为Disposable
, 来描述以上声明.
class TempFile implements Disposable{
#path: string;
#handle: number;
constructor(path: string) {
this.#path = path;
this.#handle = fs.openSync(path, "w+");
}
// other methods
[Symbol.dispose]() {
// Close file and delete it
fs.closeSync(this.#handle);
fs.unlinkSync(this.#path);
}
}
然后我们可以调用这些方法:
export function doSomeWork() {
const file = new TempFile(".some_temp_file");
try {
// ...
}
finally {
file[Symbol.dispose]();
}
}
把清理逻辑迁移到TempFile
并没有带来多少工作量, 我们只是简单地把所有在finally
代码块中执行的清理工作抽取到一个方法中, 而这总是可以做到的. 但是特定的方法名意味着JavaScript可以在其上构建其他的语言特性;
这给我们带来了第一个明星特性: using
声明! using
是一个新的关键字, 这让我们可以声明一些新的固定绑定(注:在JavaScript中, 绑定binding, 通常是指标识符和作用域之间的关联关系), 就像const
关键字. 而关键的不同是使用using
声明的变量, 会在其作用域结束时调用它们的Symbol.dispose
方法.
我们简单地改写一下代码:
export function doSomeWork() {
using file = new TempFile(".some_temp_file");
// use file...
if(someCondition)) {
// do some more work...
return;
}
}
要注意, 以上代码不再有try
/finally
代码块了! 至少, 我们看不到. 在功能上, using
声明就是准确地实现了try
/finally
代码块, 但是我们不需要自己来处理.
你可能熟悉 C#的using
声明 , Python的with
语句 , 或者 Java的try
-with-resource声明. 这些语言特性都和JavaScript新的using
关键字类似, 提供了一种在对象作用域结束时显式执行拆卸工作的方式.
using
声明会在其作用域结束时, 或者提前return
或throw
错误之前, 执行这类清理工作. 其执行次序类似堆栈的先进后出.
function 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
的一个新子类型被引入. 它使用了supressed
属性来保存最后被抛出的错误, 而error
属性用来保存清理时被抛出的错误.
class 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); // SuppressedError
console.log(e.message); // An error was suppressed during disposal.
console.log(e.error.name); // ErrorA
console.log(e.error.message); // Error from a
console.log(e.suppressed.name); // ErrorB
console.log(e.suppressed.message); // oops!
}
你可能注意到在这些示例中使用的都是同步方法. 但是大量的资源释放都涉及到异步操作, 在继续执行其他代码前, 需要等待这些异步操作完成.
这就是为什么又引入了一个新的内置symbol
- Symbol.asynDispose
, 而这给我们带来了第二个明星特性 - awaited using
声明. 该特性类似于using
声明, 但关键不同是这些声明会查阅变量的需要被await
的清理操作. 该声明使用了一个不同的方法, 名称为Symbol.asynDispose
, 当然也会执行Symbol.dispose
方法中的清理操作. 方便起见, TypeScript也引入了一个新的全局类型AsyncDisposable
来描述一个使用异步清理方法的对象.
async 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 b = 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 APIs已经定义它自己的Disposable
接口 . 在浏览器和运行时(runtimes)如Nodejs, Deno和Bun中可能也选择使用Symbol.dispose
和Symbol.asyncDispose
来命名一些对象已有的清理方法, 如文件句柄, 网络连接等.
如果你为了做了大量的专门清理工作而创建一个新类型, 可能会带来大量的过度抽象和对最佳开发实践的质疑. 例如, 让我们回到TempFile
的例子:
class 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()
方法, 或者是传入临时文件的句柄? 应该为每个可能被执行的文件操作都暴露一个方法么? 还是直接让文件对象属性公有访问?
为了解决以上问题, 带来了最后一个明星属性: DisposableStack
和 AsyncDisposableStack
. 无论是一次性清理还是任意数量的清理工作, 这些对象都非常有用. DisposableStack
对象中有几个方法可以跟踪Disposable
对象, 并且可以执行任意的清理工作. 我们也可以使用using
来该对象赋值给变量, 因为它们自己也是Disposable
对象! 所以我们来重写一下之前的示例:
function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");
using cleanup = new DisposableStack();
cleanup.defer(() => {
fs.closeSync(file);
fs.unlinkSync(path);
});
// using file...
if (domeCondition()) {
// do some more work...
return;
}
// ...
}
defer()
方法需要传入一个回调函数作为参数, 而此回调函数将在cleanup
对象被清理时执行一次. 通常, defer()
方法(以及DisposableStack
的其他方法如use
和adopt
)应该在资源被创建之后立即调用. 就像名称提示的那样, DisposableStack
会按照像堆栈一样先进后出的追踪次序来清理所有资源, 所以在创建该对象后立即执行defer
有助于避免古怪的依赖问题. AsyncDisposableStack
(注: 原文是AsyncDisposable
, 但此处结合上下文应该是AsyncDisposableStack
) 工作方式类似, 但是它追踪的是async
函数和AsyncDisposable
对象, 而它本身也是一个AsyncDisposable
对象.
defer
方法在很多方面都类似 Go, Swift, Zig, Odin 语言中的defer
关键字, 遵循类似的语言约定.
因为该特性非常新, 所以绝大多数的运行时都没有原生地支持它. 为了使用它, 你需要以下对象和类型的运行时polyfills:
Symbol.dispose
Symbol.asyncDispose
DisposableStack
AsyncDisposableStack
SuppressedError
但如果你只是想使用using
和await using
, 只需要polyfiling内置的symbol
. 通常只需以下代码, 就可以在绝大多数场景中可用:
Symbol.dispose ??= Symbol("Symbol.dispose");
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
使用时, 需要把编译target
设置为es2022
, 配置lib
设置包含"esnext"
或者 "esnext.dispsable"
.
{
"compilerOptions": {
"target": "es2022",
"lib": ["es2022", "esnext.disposable", "dom"]
}
}
关于该特性的更多信息, 参考Github PR
装饰器元数据
TypeScript 5.2实现了一项即将到来的ECMAScript特性 装饰器元数据(decorator metadata).