[[TypeScript 5.2]]で[[Explicit Resource Management]]という仕様に対応されたので試してみた。
## 背景
リソースの解放処理は各分岐の最後に実行する必要があり、忘れてしまうリスクがある。try-finallyなどで対処できなくもないが、もっとシンプルにしたい。
## 仕様の概要
- [[Symbol.dispose]]
- 破棄されたときに実行するための関数や[[メソッド (TypeScript)|メソッド]]のシグニチャ
- 後述する[[using (TypeScript)|using]]と組み合わせることで効果を発揮する
- 手動で呼び出すこともできる
- [[using (TypeScript)|using]]キーワード
- [[const (TypeScript)|const]]と似ているが、変数の破棄時に[[Symbol.dispose]]というシンボル名の関数を実行できる (確実に解放処理が実行される)
## 定義の仕方
- オブジェクトの場合
- [[Symbol.dispose]]というシンボルの関数[[プロパティ (TypeScript)|プロパティ]]を含めた値を返す
- [[クラス (TypeScript)|クラス]]の場合
- [[Symbol.dispose]]というシンボルの[[メソッド (TypeScript)|メソッド]]を定義する
- [[Disposable]]をimplementsするとよい
- 解放処理で非同期処理(await)が必要な場合
- [[Symbol.dispose]]の代わりに[[Symbol.asyncDispose]]を使う
## [[Explicit Resource Management]]が使える環境について
[[Node.js]]のv20.4.0で対応されている。
> Node is adding support to the explicit resource management proposal to its resources allowing users of TypeScript/babel to use using/await using with V8 support for everyone else on the way.
> *[Node v20.4.0 (Current)](https://nodejs.org/en/blog/release/v20.4.0)より引用*
上記の通り、[[Babel]]や[[TypeScript]]の利用が前提。つまり、[[Node.js]]ランタイムとしてはまだ[[Explicit Resource Management]]には対応していなそう。[[using (TypeScript)|using]]も構文エラーになる。
[[TypeScript 5.2]]の場合、[[TSConfig]]を以下のように設定することで動作した。
- [[lib (tsconfig)|lib]]は`["ESNext", "dom"]`
- `ESNext`でないと[[Disposable]]や[[Symbol.dispose]]が解決しない
- [[lib (tsconfig)|lib]]は厳密には `["ES2022", "esnext.esnext.disposable", "dom"]` でOK
- `ES2023`でもいけそうな気がしたけどダメだった (5.2.2ではまだ認識しなかった)
- [[target (tsconfig)|target]]は`ESNext`以外 (`ES2022`など)
- `ESNext`だと[[using (TypeScript)|using]]パラメータが残るため、[[Node.js]]のランタイムエラーになる
## 動作確認
コードを書く前に[[.prettierrc]]を作成する。
```json
{
"overrides": [
{
"files": "*.ts",
"options": {
"parser": "babel-ts"
}
}
]
}
```
[[Prettier]] 5.0.2時点では[[using (TypeScript)|using]]キーワードがあるとフォーマットがかからないという問題がある。これは [Support TypeScript 5\.2 · Issue \#15004 · prettier/prettier](https://github.com/prettier/prettier/issues/15004) で対応される予定とのことだが、`parser`に`babel-ts`を設定することで動作させることが可能。メンテナの方から教えていただいた。感謝🙏

### クラス
```ts
class DisposableHuman implements Disposable {
constructor(
public id: number,
public name: string
) {}
[Symbol.dispose]() {
console.log("DisposableHumanを開放します!");
}
}
console.log("Before in scope");
{
console.log("In scope");
using ichiro = new DisposableHuman(1, "Ichiro");
console.log("Before out scope");
}
console.log("After out scope");
```
実行結果。
```console
Before in scope
In scope
Before out scope
DisposableHumanを開放します!
After out scope
```
### オブジェクト
```ts
console.log("start main")
{
console.log("start scope")
const getResource = (): Disposable => {
return {
[Symbol.dispose]: () => {
console.log('resourceがスコープから外れたらこれが実行される')
}
}
}
using resource = getResource();
console.log("end scope")
}
console.log("end main")
```
実行結果。
```console
start main
start scope
end scope
resourceがスコープから外れたらこれが実行される
end main
```
### 非同期
```ts
async function asyncHello(): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Hello");
resolve();
}, 500);
});
}
class DisposableHuman implements AsyncDisposable {
constructor(
public id: number,
public name: string
) {}
async [Symbol.asyncDispose](): Promise<void> {
console.log(`start asyncDispose: ${this.name}`);
await asyncHello();
console.log(`end asyncDispose: ${this.name}`);
}
}
console.log("Before in scope");
{
(async () => {
{
console.log("In scope Ichiro");
await using ichiro = new DisposableHuman(1, "Ichiro");
console.log("Before out scope Ichiro");
}
{
console.log("In scope Jiro");
await using jiro = new DisposableHuman(2, "Jiro");
console.log("Before out scope Jiro");
}
})();
}
console.log("After out scope");
```
実行結果。`ichiro`のスコープを抜けて非同期のdisposeが完了するまで、次の`jiro`スコープには入らない。
```console
Before in scope
In scope Ichiro
Before out scope Ichiro
start asyncDispose: Ichiro
After out scope
Hello
end asyncDispose: Ichiro
In scope Jiro
Before out scope Jiro
start asyncDispose: Jiro
Hello
end asyncDispose: Jiro
```
[[Symbol.asyncDispose]]を使わず、[[Symbol.dispose]]をasyncで非同期にしただけだと挙動が変わる。
```ts
async function asyncHello(): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Hello");
resolve();
}, 500);
});
}
class DisposableHuman implements Disposable {
constructor(
public id: number,
public name: string
) {}
async [Symbol.dispose](): Promise<void> {
console.log(`start asyncDispose: ${this.name}`);
await asyncHello();
console.log(`end asyncDispose: ${this.name}`);
}
}
console.log("Before in scope");
{
(async () => {
{
console.log("In scope Ichiro");
using ichiro = new DisposableHuman(1, "Ichiro");
console.log("Before out scope Ichiro");
}
{
console.log("In scope Jiro");
using jiro = new DisposableHuman(2, "Jiro");
console.log("Before out scope Jiro");
}
})();
}
console.log("After out scope");
```
実行結果。`ichiro`スコープを抜けたときのdisposeを待たずして、次の`jiro`スコープに入ってしまう。`ichiro`のdisposeが終わる前に`jiro`の処理が始まるので、場合によっては大変なことになる。
```console
Before in scope
In scope Ichiro
Before out scope Ichiro
start asyncDispose: Ichiro
In scope Jiro
Before out scope Jiro
start asyncDispose: Jiro
After out scope
Hello
end asyncDispose: Ichiro
Hello
end asyncDispose: Jiro
```
これはエラーで気づくことはできないから要注意ポイントだと思った。
### disposeの処理をコールバックとして指定
[[DisposableStack]]を利用する。
[[Node.js]] v20.4.0時点ではランタイムが対応していないため、[[es-shims]]を利用する。
<div class="link-card">
<div class="link-card-header">
<img src="https://github.githubassets.com/favicons/favicon.svg" class="link-card-site-icon"/>
<span class="link-card-site-name">GitHub</span>
</div>
<div class="link-card-body">
<div class="link-card-content">
<div>
<p class="link-card-title">GitHub - es-shims/DisposableStack: An ESnext spec-compliant `DisposableStack`, `AsyncDisposableStack`, `Symbol.dispose`, and `Symbol.asyncDispose` shim/polyfill/replacement that works as far down as ES3.</p>
</div>
<div class="link-card-description">
An ESnext spec-compliant `DisposableStack`, `AsyncDisposableStack`, `Symbol.dispose`, and `Symbol.as...
</div>
</div>
<img src="https://opengraph.githubassets.com/7c5a4f1a987834ab6bd413fd4b5f55d075bac661564263f881a32e252b4a5313/es-shims/DisposableStack" class="link-card-image" />
</div>
<a href="https://github.com/es-shims/DisposableStack"></a>
</div>
```console
npm install --save disposablestack
```
[[TypeScript]]の型定義としては`lib.esnext.disposable.d.ts`のものを使いたくて、ランタイム時は`disposablestack`を使いたいので、以下のようにimport文を追加する。
```ts
import "disposablestack/auto"
class Human {
constructor(
public id: number,
public name: string
) {}
}
console.log("Before in scope");
{
console.log("In scope");
const ichiro = new Human(1, "Ichiro");
using cleanup = new DisposableStack();
cleanup.defer(() => {
console.log("スコープの後処理");
});
console.log("Before out scope");
}
console.log("After out scope");
```
実行結果。
```console
Before in scope
In scope
Before out scope
スコープの後処理
After out scope
```