Menu

TypeScriptのデコレーター: メソッド・クラス・フィールド

デコレーターは、@ 構文でクラスのメンバーをラップしたり置き換えたりする関数です。TypeScriptがフラグなしでサポートする標準のデコレーター(クラス、メソッド、getter、フィールド、accessor)、デコレーターファクトリー、addInitializer、そして Angular や NestJS が使う従来の experimentalDecorators との違いを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

デコレーターは、@name でクラスやクラスのメンバーに付ける関数です。元のメソッド(またはクラス、フィールド)と、それを説明するコンテキストオブジェクトを受け取り、置き換えるものを返せます。TypeScriptはコンパイラーのフラグなしで標準のデコレーターをサポートしています。

@logged はクラスが定義されたときに一度だけ実行され、add を返したラッパーに置き換えます。すべての呼び出しはラッパーを通ります。ジェネリクスの型パラメーターがメソッドの this、引数、戻り値の型をそのまま保つので、add は相変わらず2つの数値を受け取って数値を返します。

デコレーターのコンパイル結果

標準のデコレーターはJavaScriptの TC39 の提案から来ていて、TypeScriptは TypeScript 5.0 から実装しています。この提案はまだJavaScriptの標準に入っておらず、Node 24 は @ の構文を解析できません。そのため、ターゲットが ES2022 のとき(このページのサンプルもそうです)、コンパイラーはデコレートされたクラスを、ヘルパー関数(ファイルの先頭に出力される __esDecorate と __runInitializers)を呼ぶ普通のJavaScriptに書き換えます。出力は ES2022 が動くところならどこでも動きます。

その結果として、デコレーターを含むファイルは Node の組み込みの型除去(node file.ts)では実行できません。型除去は型を取り除くだけで @ は残すからです。Node は SyntaxError: Invalid or unexpected token で止まります。先に tsc かバンドラーでコンパイルしましょう。

デコレーターの種類とシグネチャ

標準のデコレーターはすべて (value, context) => replacement | void という形です。value が何で、何を返せるかは、デコレートする対象によって変わります:

デコレートする対象valueコンテキストの型戻り値
クラスクラスClassDecoratorContext置き換えるクラス、または何も返さない
メソッドメソッドClassMethodDecoratorContext置き換えるメソッド
getter / settergetter または setterClassGetterDecoratorContext / ClassSetterDecoratorContext置き換える getter または setter
フィールドundefinedClassFieldDecoratorContext初期値を変換する関数
accessor フィールド{ get, set }ClassAccessorDecoratorContext{ get?, set?, init? }

どのコンテキストオブジェクトも kind、name、addInitializer を持ちます。クラスのメンバーのコンテキストは、さらに static、private、インスタンスからメンバーを読むための access オブジェクトを持ちます。デコレーターは static メンバーや #private メンバーにも使えます。

デコレーターファクトリー

オプションを渡すには、デコレーターを返す関数を書き、@ の位置で呼び出します。これがデコレーターファクトリーです:

@retry(3) はまず retry を呼び、それが返す関数が実際のデコレーターになります。デコレーターは重ねられます。@a @b method() では、b が先に適用され、その結果を a がラップします。

クラスデコレーターと addInitializer

クラスデコレーターはクラスそのものを受け取ります。サブクラスを返してクラスを置き換えることも、何も返さずにどこかに記録するだけにすることもできます。context.addInitializer は、特定のタイミングで実行するコードを登録します。クラスデコレーターならクラスが完全に定義された直後、メソッドデコレーターなら各インスタンスが作られるときです。

クラスデコレーターはクラスの上(または export のあと)に書きます。@bound がなければ、this が undefined になるので、loose() の呼び出しは例外を投げます。

フィールドデコレーターと accessor デコレーター

フィールドデコレーターは、あとからの代入を見ることも横取りすることもできません。value は undefined で、返せるのはフィールドの初期値を変換する関数だけです。読み書きを横取りするには、フィールドを accessor キーワードで宣言し(プライベートな領域に値を持つ getter と setter のペアになります)、それをデコレートします:

accessor は同じ提案の一部です。#private フィールドの上に本物の getter と setter を出力するので、p.price = -5 はデコレーターの set を通ります。

標準のデコレーターと従来の experimentalDecorators

TypeScript 5.0 より前にTypeScriptが持っていたデコレーターは、experimentalDecorators で有効にする初期版の提案だけでした。このフラグは今もあり、コンパイラーをシグネチャも意味も違う従来のモデルに切り替えます:

標準(フラグなし)従来(experimentalDecorators)
シグネチャ(value, context)(target, propertyKey, descriptor)
メソッドの変え方新しい関数を返すdescriptor.value を書き換える
引数デコレーター非対応(TS1206)対応
emitDecoratorMetadata非対応対応(reflect-metadata による実行時の型情報)
accessor デコレーター({ get, set, init })、addInitializerありなし
元になったものTC39 の提案その古い草案

一方のモデル用に書いたデコレーターは、もう一方では型チェックを通りません。フラグのないプロジェクトで従来型のデコレーターを使うと次のようになります:

index.ts(11,5): error TS1241: Unable to resolve signature of method decorator when called as an expression.
  The runtime will invoke the decorator with 2 arguments, but the decorator expects 3.

直すには、このページの最初の例のように標準の (value, context) の形に書き直すか、プロジェクト全体で従来のモデルを有効にします:

{
    "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

Angular、NestJS、TypeORM は、ツールが生成する設定やドキュメントで今も experimentalDecorators を指定しています。NestJS と TypeORM は実行時に型を読むので emitDecoratorMetadata も必要です。NestJS はコンストラクターの引数を注入するため、TypeORM はプロパティをカラムに対応づけるためです:

// Legacy model: needs experimentalDecorators (and emitDecoratorMetadata for DI).
@Injectable()
class UsersService {
    constructor(@Inject(DB) private db: Database) {}
}

これらのフレームワークを使うなら、デコレーターは従来の方法で書き、そのドキュメントに従いましょう。そうしたフレームワークを使わない新しいコードでは、標準のデコレーターを使います。

デコレーターを使う場面

デコレーターは、そうしなければ多くのメソッドで繰り返すことになる横断的な処理に向いています。ログ、時間の計測、キャッシュ、リトライ、アクセスチェック、検証、コンテナーやルーターへのクラスの登録などです。一方で制御の流れを隠してしまうので、読む人はメソッドが何をするかを知る前に @retry が何をするかを調べなければなりません。使う場所が1つか2つなら、普通の高階関数(const fetchData = retry(3, rawFetch))のほうがシンプルで、クラスの外でも使えます。

よくある質問

TypeScriptのデコレーターとは何ですか?

デコレーターは、@name 構文でクラスやクラスのメンバーに適用する関数です。デコレートされるものとコンテキストオブジェクトを受け取り、置き換えるもの(ラップしたメソッド、新しいクラス、フィールドの初期値を変換する関数)を返せます。よくある用途はログ、検証、キャッシュ、クラスの登録です。

TypeScriptでデコレーターを使うには experimentalDecorators が必要ですか?

必要ありません。TypeScript 5.0 以降、標準(TC39)のデコレーターはフラグなしで動きます。experimentalDecorators はコンパイラーを古い従来のデコレーターのモデルに切り替えるもので、Angular や NestJS のようなフレームワークはこちらを前提にしています。2つは関数のシグネチャが違い、互換性はありません。

標準のデコレーターと experimentalDecorators の違いは何ですか?

標準のデコレーターは (value, context) を受け取り、置き換えるものを返します。従来のデコレーターは (target, propertyKey, descriptor) を受け取り、プロパティディスクリプターを書き換えます。引数デコレーターと emitDecoratorMetadata に対応しているのは従来のモデルだけで、{ get, set, init } を返す accessor デコレーターと context.addInitializer があるのは標準のモデルだけです。

TypeScriptは引数デコレーターに対応していますか?

experimentalDecorators を有効にした場合だけです。標準モードでは、引数へのデコレーターはエラー TS1206: Decorators are not valid here になります。TC39 の提案に引数デコレーターが含まれていないからです。そのため、コンストラクターの引数をデコレートする依存性注入のフレームワークには従来のフラグが必要です。

複数のデコレーターはどの順序で適用されますか?

デコレーターの式は上から下へ評価されますが、適用は下から上です。@a @b method() では、まず b がメソッドをラップし、その結果を a がラップします。そのため、メソッドを呼ぶと a がいちばん外側で実行されます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める