TypeScript v5.2で導入されるusing宣言とDecorator Metadataを使ってみる

| 7 min read
Author: noboru-kudo noboru-kudoの画像

少し前にTypeScript v5.2のベータバージョンがリリースされました。

いくつか注目機能の導入がありましたので、試してみた結果をご紹介します。

Caution

今回紹介するものは、現時点ではJavaScriptランタイム環境でサポートが追いついていないものがほとんどです。
このため、ここでのサンプルソースコードはCoreJSでPolyfillしたものを使っています。

また、v5.2のベータバージョンを使っていますので、利用する場合は最新の状況を確認してください(安定版は2023-08リリース予定です)。

using宣言によるリソース解放(クリーンアップ)

#

以下ECMAScriptプロポーザルのTypeScript実装です。

try-finallyを使わなくとも、確実にリソースの解放をしてくれます。
Javaのtry-with-resourceやC#のusing構文など、他の言語ではかなり前からありましたが、(意外にも)JavaScript/TypeScriptにはありませんでした。

この機能自体は、以下Zennの記事で非常に詳しく説明されています。詳しく知りたい方はこちらを読むのが一番良いと思います。

本記事はポイントのみ紹介します。

いきなりですが、以下のようなソースコードになります。

class Connection implements Disposable {

  database: string | null; // サンプルのリソース管理対象

  constructor() {
    this.database = 'my database';
    console.log('initialize!!')
  }
  update(): void {
    this.database = 'foo database'
    console.log('update my database!!')
  }
  // Disposableの実装
  [Symbol.dispose]() {
    // クリーンアップ処理
    this.database = null;
    console.log('connection released!!')
  }
}

function updateDB() {
  // v5.2で追加されたusing宣言
  using conn = new Connection();
  conn.update();
} // スコープを抜けると[Symbole.dispose]()が実行されてクリーンアップされる

リソース解放するオブジェクトは、Disposableインターフェースをimplementsする必要があります。
Disposableインターフェースの定義は、以下のようになっています。

interface Disposable {
    [Symbol.dispose](): void;
}

[Symbol.dispose]()のみが定義されています。ここでクリーンアップ処理を実装します。
このオブジェクトを新たに追加されたusingを使って変数宣言すると、スコープを抜けるタイミングでこのクリーンアップ処理が実行されます。

もちろん非同期用のAsyncDisposableインターフェースも用意されています。

interface AsyncDisposable {
    [Symbol.asyncDispose](): PromiseLike<void>;
}

クリーンアップ処理が非同期な場合は、こちらを使うことになります。
この場合は、以下のようにusingの前にawaitをつける必要があります。

async function updateDB() {
  await using conn = new Connection();
  conn.update();
}
DisposableStackでクリーンアップ処理を記述する

個別にDisposableを実装せずに、もっと柔軟にクリーンアップ処理を記述するDisposableStack(またはAsyncDisposableStack)もサポートされます。
対象オブジェクトを直接変更できない場合や、複数のクリーンアップ処理をまとめるなど柔軟な使い方ができます。

これはGo言語やSwiftで使われるDefer構文を使ったクリーンアップ処理と似ています。
先ほどのソースコードをDisposableStackで書き直すと、以下のようになります。

function updateDB() {
  let database: string | null = 'my database';
  using cleanup = new DisposableStack()
  cleanup.defer(() => {
    // クリーンアップ処理
    database = null;
    console.log('connection released!!')
  })
  // update
  database = 'foo database'
} // スコープを抜けるタイミングでdeferが実行される

using宣言でDisposableStackを生成し、これのdeferにクリーンアップ処理を実装します。
この場合でもDisposableStackのインスタンスがスコープを抜けるタイミングで、クリーンアップ処理が実行されます。つまりDisposableStackがDisposableインターフェースを実装しています。
検証していませんが、DisposableStackにはdefer以外も様々なメソッドが用意されていますので、様々なユースケースに対応するクリーンアップ処理が実装できそうです。

Decorator Metadata

#

以下の記事でも紹介していますが、Decorator(Stage 3)自体はv5.0で正式に導入されています。

ですが、この時点でメタデータAPIは別プロポーザルに切り出されて先送りされていました。
このメタデータAPIがStage 3となり、TypeScript v5.2で導入されることになりました。

早速ソースコードを書いてみます。
以下のように仮想ORマッパーのエンティティクラスを想定してみます。

class Blog {
  @Column({ name: 'ID', type: 'int', notNull: true })
  id: number;
  @Column({ name: 'TITLE', type: 'varchar', notNull: true })
  title: string;
  @Column({ name: 'CONTENT', type: 'varchar', notNull: true })
  content: string;
  @Column({ name: 'STAR', type: 'int', notNull: false })
  star: number;

  constructor(id: number, title: string, content: string, star: number) {
    this.title = title;
    this.id = id;
    this.content = content;
    this.star = star;
  }
}

フィールドにDecorator(@Column)がついています。引数としてカラム属性を設定しています。
今回はこの属性をメタデータとして利用するものとします。

対応するDecoratorの関数は以下のようになります。

type Attr = { name: string, type: 'varchar' | 'char' | 'int', notNull: boolean }

function Column(attr: Attr) {
  return function(target: any, context: ClassFieldDecoratorContext) {
    context.metadata[context.name] = attr;
  };
}

地味ですが、メタデータAPIとしてcontext.metadataが追加されました。
このメターデータの型はDecoratorMetadataで、以下の定義となっています。

declare type PropertyKey = string | number | symbol;
type DecoratorMetadataObject = Record<PropertyKey, unknown> & object;

type DecoratorMetadata =
    typeof globalThis extends { Symbol: { readonly metadata: symbol } } ? DecoratorMetadataObject : DecoratorMetadataObject | undefined;

条件付きタイプが使われていて、少し分かりにくいですがRecord<PropertyKey, unknown>となっているところを見るとキーバリュー形式で使えるものと思って良さそうです。
このメターデータは、該当クラスの全てのDecorator関数で同じものが使われます。
ここでは、シンプルにメタデータに引数として渡されている属性情報を保存しています。

次に外部からこのメタデータにアクセスしてみます。

const metadata = Blog[Symbol.metadata] ?? {};
Object.keys(metadata).forEach((key) => {
  console.log(`${key} -> ${JSON.stringify(metadata[key])}`);
});
// id -> {"name":"ID","type":"int","notNull":true}
// title -> {"name":"TITLE","type":"varchar","notNull":true}
// content -> {"name":"CONTENT","type":"varchar","notNull":true}
// star -> {"name":"STAR","type":"int","notNull":false}

メタデータはクラスに紐づいていますので、上記のようにクラスのSymbol.metadataから取得できます(Symbol.metadataもv5.2で追加されました)。
ここでは取得したメタデータをそのままログに出力しています。

メタデータをプライベートに扱う

上記コードはメタデータはパブリック扱いです(どこからでもクラスから参照できる)。
冒頭のTypeScriptブログTC39プロポーザルではWeakMapを使ってメタデータの格納をプライベートにする方法を紹介しています。

この場合は以下のようなソースコードになります。

// エンティティのソースコードは変更なし

// 仮想プライベートな変数
const ATTRIBUTES = new WeakMap();
function Column(attr: Attr) {
  return function(target: any, context: ClassFieldDecoratorContext) {
    // メタデータをキーとしてクラスに紐づくメタデータを取得
    // コンテキストのメタデータには何も格納してない
    let metadata = ATTRIBUTES.get(context.metadata);

    if (!metadata) {
      metadata = {};
      ATTRIBUTES.set(context.metadata, metadata);
    }

    metadata[context.name] = attr;
  };
}

// クラスに紐づくメタデータを取得
const metadata = ATTRIBUTES.get(Blog[Symbol.metadata] ?? {});
Object.keys(metadata).forEach((key) => {
  console.log(`${key} -> ${JSON.stringify(metadata[key])}`);
});

この場合はDecorator関数では、メタデータ(context.metadata)ではなくプライベートな変数(上記ではATTRIBUTES)に保存しています。

まとめ

#

using宣言などTypeScript v5.2も重要なECMAScriptプロポーザルが導入されています。
一般的に使われ出すのはもう少し先になりますが、乗り遅れないようしっかり予習しておきたいものですね。

豆蔵では共に高め合う仲間を募集しています!

recruit

具体的な採用情報はこちらからご覧いただけます。