[[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`を設定することで動作させることが可能。メンテナの方から教えていただいた。感謝🙏 ![](https://twitter.com/__sosukesuzuki/status/1695731372034494861?s=20) ### クラス ```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 ```