クラスやメソッドに「後付けで機能を足したい」と思ったことはないでしょうか。たとえば、あるメソッドが呼ばれるたびにログを残したい、あるクラスに共通の初期化処理を差し込みたい、といった場面です。こうした処理を本来のロジックと分けてスマートに書けるのがデコレーター(Decorators)です。この記事では、TypeScript 5.0 以降で使えるようになった標準デコレーター(TC39 Stage 3 準拠)を前提に、基本の書き方から、デコレーター関数が受け取る value と context の意味、実際に動くログ取得デコレーター、引数を受け取るデコレーターファクトリ、そしてつまずきやすい設定や実行順序までを、具体的なコードとあわせて解説します。
目次
デコレーターとは何か・何ができるか
デコレーターは、クラスやクラスのメンバー(メソッド・フィールドなど)に「印」を付けて、そこに機能を追加・変更するための仕組みです。@ から始まる名前を、クラスやメソッドの直前に書いて使います。正体はただの関数で、デコレーターを付けた対象がどんなものかを受け取り、必要なら差し替えたり、初期化処理を仕込んだりします。
ポイントは、本来のロジックにはいっさい手を入れずに、外側から機能を足せることです。たとえば「このメソッドが呼ばれたらログを残す」という処理を、メソッド本体に console.log を書き足すのではなく、@logged という印を1行付けるだけで実現できます。ログの有無を切り替えたいときも、印を付けたり外したりするだけで済み、本来の処理は汚れません。
なお、ここで扱うのは TypeScript 5.0 から標準搭載された新しいデコレーターです。かつて experimentalDecorators という設定を有効にして使っていた旧仕様とは、関数が受け取る引数の形が異なります。両者の違いは記事の後半で触れます。
クラスとメソッドに付ける基本の書き方
まずは最小の例から見ていきます。デコレーターは関数として定義し、その関数名の前に @ を付けて、クラスやメソッドの直前に置きます。次の sealed はクラスデコレーターの例で、クラスが定義されたタイミングで一度だけ呼ばれます。
// クラスデコレーター:クラス本体(コンストラクタ)と context を受け取る
function sealed(value: Function, context: ClassDecoratorContext) {
console.log(`${String(context.name)} クラスが定義されました`);
}
@sealed
class User {
constructor(public name: string) {}
}
// 出力: User クラスが定義されました
クラスデコレーターは、対象のクラス(コンストラクタ関数)を第1引数 value として受け取ります。上の例では受け取るだけで何もしていませんが、必要ならここでクラスを拡張したり、別のクラスに差し替えたりできます。
次はメソッドデコレーターです。メソッドの直前に @ 付きで書くと、そのメソッド1つに対してデコレーターが適用されます。
// メソッドデコレーター:元のメソッド関数と context を受け取る
function trace(value: Function, context: ClassMethodDecoratorContext) {
console.log(`${String(context.name)} メソッドが登録されました`);
}
class Calculator {
@trace
add(a: number, b: number) {
return a + b;
}
}
// 出力: add メソッドが登録されました
ここで注意したいのは、デコレーターが呼ばれるのはクラスが定義されるとき(一度だけ)であり、メソッドが実際に呼び出されるたびではない、という点です。「メソッドが呼ばれるたびに何かしたい」場合は、次の実践例で見るように、デコレーターの中で元のメソッドを包んだ新しい関数を返す必要があります。
デコレーター関数が受け取る value と context
標準デコレーターの関数は、共通して2つの引数を受け取ります。第1引数の value は「デコレーターを付けた対象そのもの」で、クラスデコレーターならクラス(コンストラクタ)、メソッドデコレーターなら元のメソッド関数が入ります。第2引数の context は「その対象に関する情報」をまとめたオブジェクトです。
context オブジェクトが持つ主なプロパティを、下の表にまとめます。デコレーターの種類(クラス・メソッド・フィールドなど)によって context の型は変わりますが、共通して使えるものが多くあります。
| プロパティ | 意味 |
|---|---|
kind | デコレーターの種類を表す文字列。"class" / "method" / "getter" / "setter" / "field" / "accessor" のいずれか |
name | 対象の名前。メソッド名やクラス名(文字列またはシンボル) |
static | その要素が静的メンバー(static)かどうかの真偽値。クラスデコレーターにはない |
private | その要素がプライベートメンバー(#name)かどうかの真偽値。クラスデコレーターにはない |
addInitializer | 初期化時に実行したい処理を登録する関数。インスタンス生成時やクラス定義時に呼ばれる |
access | 対象の値を取得・設定するための get / set を持つオブジェクト |
特に context.name はログ出力などで対象を識別するのによく使い、context.addInitializer はインスタンス生成時に処理を差し込みたいとき(後述のメソッドの自動バインドなど)に便利です。デコレーター関数の戻り値も種類ごとに意味が決まっており、メソッドデコレーターで新しい関数を返すと、元のメソッドがその関数に置き換わります。この「関数を返して差し替える」仕組みが、次の実践例の要になります。
実践例:メソッドの実行ログを取る @logged
ここまでの知識を使って、「メソッドが呼ばれるたびに、引数と戻り値をログ出力する」デコレーターを作ってみます。ポイントは、元のメソッド value を包んだ新しい関数を return することです。戻り値の関数が元のメソッドを置き換えるため、以降そのメソッドを呼ぶと、ラップした処理が実行されます。
function logged(value: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
// 元のメソッドを包んだ新しい関数を返す
function replacement(this: any, ...args: any[]) {
console.log(`${methodName}(${args.join(', ')}) を呼び出し`);
const result = value.call(this, ...args); // 元のメソッドを実行
console.log(`${methodName} は ${result} を返しました`);
return result;
}
return replacement; // 元のメソッドがこの関数に置き換わる
}
class Calculator {
@logged
add(a: number, b: number) {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// 出力:
// add(2, 3) を呼び出し
// add は 5 を返しました
ここで大切なのは value.call(this, ...args) の部分です。元のメソッドを呼ぶとき、this を正しく渡さないと、メソッド内で this.xxx を使っている場合に壊れてしまいます。replacement 関数を通常の関数(アロー関数ではない)で書き、value.call(this, ...) で呼び出し元の this を引き継ぐのが定石です。引数(...args)と戻り値(result)を横取りしてログに出しつつ、最後は元の結果をそのまま返しているので、呼び出し側の動作は変わりません。
引数を受け取るデコレーターファクトリ
先ほどの @logged は固定の動きでしたが、「ログに付ける接頭辞を変えたい」のように、デコレーターに設定を渡したくなることがあります。そのときに使うのがデコレーターファクトリです。これは「デコレーターを返す関数」で、@名前(引数) のように呼び出して使います。外側の関数で引数を受け取り、その中で本来のデコレーター関数を返す、という二段構えになります。
// 外側の関数で引数(prefix)を受け取る
function logged(prefix: string) {
// 中でデコレーター本体を返す
return function (value: Function, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.log(`${prefix} ${methodName} を呼び出し`);
return value.call(this, ...args);
};
};
}
class Api {
@logged('[DEBUG]')
fetchUser(id: number) {
return { id };
}
}
new Api().fetchUser(1);
// 出力: [DEBUG] fetchUser を呼び出し
@logged('[DEBUG]') のように括弧付きで書くと、まず logged('[DEBUG]') が実行され、その戻り値であるデコレーター関数が fetchUser に適用されます。引数なしのデコレーター(@logged)と、引数付きのデコレーターファクトリ(@logged('[DEBUG]'))は、括弧を付けるかどうかで見分けられます。設定を渡せるようにしたいときは、このファクトリ形式を選びましょう。
tsconfig の設定とバージョンに注意する
標準デコレーターは TypeScript 5.0 以降で使えます。特別な設定を追加しなくても動きますが、いくつか気を付ける点があります。
experimentalDecorators は有効にしない
tsconfig.json で experimentalDecorators を true にすると、この記事で紹介した標準デコレーターではなく、旧仕様のデコレーターが有効になります。旧仕様では関数が受け取る引数が (target, propertyKey, descriptor) という別の形になり、context オブジェクトは渡ってきません。標準デコレーターを使いたいなら、このオプションは付けない(または false)ようにします。
ターゲット(target)が古すぎないか確認する
コンパイル先の target が極端に古いと、デコレーターの変換がうまく働かないことがあります。標準デコレーターを使うなら、target は ES2015 以降にしておくと安心です。デコレーター自体は下位のターゲットへもトランスパイルされますが、まずは新しめのターゲットで動作を確認するのがおすすめです。
複数のデコレーターを付けたときの実行順序
1つの対象に複数のデコレーターを重ねて付けることもできます。このとき順序に少し独特なルールがあり、勘違いしやすいポイントです。デコレーターの式の評価は上から下(書いた順)ですが、実際に対象へ適用されるのは下から上(内側から外側)の順になります。
function first() {
console.log('first: 評価');
return function (value: Function, context: ClassMethodDecoratorContext) {
console.log('first: 適用');
};
}
function second() {
console.log('second: 評価');
return function (value: Function, context: ClassMethodDecoratorContext) {
console.log('second: 適用');
};
}
class Example {
@first()
@second()
method() {}
}
// 出力:
// first: 評価
// second: 評価
// second: 適用
// first: 適用
ファクトリの引数を評価する部分(first: 評価 / second: 評価)は書いた順に上から実行され、返ってきたデコレーターが対象に適用される部分(second: 適用 / first: 適用)は下から順に実行されます。メソッドを差し替えるデコレーターを複数重ねる場合、内側(下)のデコレーターが先に元のメソッドを包み、その結果を外側(上)がさらに包む、という入れ子構造になります。ログ・認可・キャッシュなど複数の関心事を重ねるときは、この順序を意識すると挙動を追いやすくなります。
旧 experimentalDecorators との非互換に注意
Web 上には、experimentalDecorators を前提にした古い記事やライブラリが数多く残っています。旧仕様のデコレーターは第2引数に propertyKey、第3引数に PropertyDescriptor を受け取る形で、標準デコレーターの (value, context) とは完全に別物です。両者はソースコードの互換性がないため、標準デコレーターの書き方をそのまま旧環境に貼っても動きませんし、その逆も同様です。
また、Angular や旧 NestJS などのフレームワークは、長らく experimentalDecorators と reflect-metadata を前提にした独自のデコレーターを使ってきました。こうしたフレームワークを使っているプロジェクトでは、標準デコレーターに切り替える前に、そのフレームワークが標準デコレーターへ対応済みかどうかを必ず確認してください。サンプルコードを見るときも、それが標準デコレーター向けなのか旧仕様向けなのかを、引数の形(context があるか、descriptor があるか)で見分けるようにすると混乱を避けられます。
まとめ
デコレーターは、クラスやメソッドに @名前 の印を付けて、本来のロジックを汚さずに機能を後付けできる仕組みです。TypeScript 5.0 以降の標準デコレーターでは、関数が第1引数 value(対象そのもの)と第2引数 context(kind / name / addInitializer などの情報)を受け取ります。メソッドの動きを変えたいときは、元のメソッドを value.call(this, ...args) で呼ぶ新しい関数を返して差し替えるのが基本で、これでログ取得のような処理が書けます。引数を渡したいときは「デコレーターを返す関数」であるデコレーターファクトリを使います。注意点として、標準デコレーターでは experimentalDecorators を有効にしないこと、複数付けたときは評価が上から・適用が下から行われること、そして experimentalDecorators を前提とした旧仕様とは引数の形が非互換であることを押さえておきましょう。