【TypeScript】デコレータ(Decorators)完全ガイド|クラス・メソッド・プロパティ装飾と実務活用パターンを徹底解説

TypeScriptのデコレータ(Decorators)は、クラス・メソッド・プロパティ・パラメータに宣言的な注釈を付けて機能を追加できる強力な機能です。Angular・NestJSでは不可欠な構文であり、ログ・バリデーション・依存性注入(DI)などのパターンをエレガントに実装できます。

本記事では、4種類のデコレータの書き方から実行順序・ファクトリ・実務活用まで、実例コードとともに徹底解説します。

スポンサーリンク

デコレータとは

デコレータは @expression の形式で記述し、クラスや그 메서드에 기능을 추가하는仕組みです。デコレータはメタプログラミングの一種で、コードの振る舞いを宣言的に変更できます。

デコレータの種類
① クラスデコレータ   ② メソッドデコレータ   ③ アクセサデコレータ   ④ プロパティデコレータ   ⑤ パラメータデコレータ
@sealed                    // クラスデコレータ
class BugReport {
  @format("Hello")         // プロパティデコレータ
  title: string;

  @configurable(false)     // メソッドデコレータ
  greet(@required msg: string) {  // パラメータデコレータ
    return msg;
  }
}

デコレータは 実行時(ランタイム) に評価され、クラス定義が読み込まれた時点で適用されます。

tsconfig設定

従来のデコレータを使うには tsconfig.json で以下を有効化します。(TypeScript 5.0以降の新デコレータ仕様では不要な設定もあります)

{
  "compilerOptions": {
    "target": "ES2017",
    "experimentalDecorators": true,   // デコレータ有効化
    "emitDecoratorMetadata": true      // メタデータ出力(DI等に必要)
  }
}
注意: experimentalDecorators
名前の通り「実験的」機能です。TypeScript 5.0でStage 3の新デコレータ仕様が追加されましたが、NestJS・Angular等の既存フレームワークは引き続き experimentalDecorators: true を使います。詳細は TypeScript 5.0の新デコレータ をご参照ください。

また、emitDecoratorMetadata を使う場合は reflect-metadata パッケージのインストールが必要です。

npm install reflect-metadata
# エントリポイントでimport
import "reflect-metadata";

tsconfig全体の設定については tsconfig.json 完全ガイド も参照してください。

クラスデコレータ

クラスデコレータはクラス宣言の直前に置かれ、クラスのコンストラクタを引数として受け取ります。

// クラスデコレータの型
type ClassDecorator = (target: Function) => void | Function;

// シンプルなクラスデコレータ
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
  console.log(`${constructor.name} クラスをシールしました`);
}

@sealed
class BugReport {
  type = "report";
  title: string;

  constructor(t: string) {
    this.title = t;
  }
}

const b = new BugReport("TypeScriptエラー");
// BugReport クラスをシールしました → コンストラクタへの追加不可

コンストラクタを差し替えてクラスを拡張することもできます:

function withTimestamp<T extends { new (...args: any[]): {} }>(Base: T) {
  return class extends Base {
    createdAt = new Date().toISOString();
  };
}

@withTimestamp
class User {
  constructor(public name: string) {}
}

const u = new User("Alice") as User & { createdAt: string };
console.log(u.createdAt); // "2024-01-15T10:30:00.000Z"

メソッドデコレータ

メソッドデコレータは3つの引数を受け取ります:

引数 説明
target any クラスのプロトタイプ(static メソッドはコンストラクタ関数)
propertyKey string | symbol メソッド名
descriptor PropertyDescriptor プロパティディスクリプタ(value, writable, enumerable, configurable)
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`[${propertyKey}] 引数:`, args);
    const result = original.apply(this, args);
    console.log(`[${propertyKey}] 戻り値:`, result);
    return result;
  };

  return descriptor;
}

class Calculator {
  @log
  add(a: number, b: number): number {
    return a + b;
  }
}

const calc = new Calculator();
calc.add(2, 3);
// [add] 引数: [2, 3]
// [add] 戻り値: 5
ポイント: descriptor.value を差し替えてメソッドをラップ
元のメソッドを保存してから新しい関数で包む(ラッパーパターン)。apply(this, args) で元の this コンテキストを維持することが重要です。

プロパティデコレータ

プロパティデコレータはクラスのプロパティ宣言の直前に記述します。引数は target(プロトタイプ)と propertyKey(プロパティ名)の2つです。

function readonly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false,
    configurable: false,
  });
}

class Config {
  @readonly
  version = "1.0.0";
}

const config = new Config();
// config.version = "2.0.0"; // エラー: 書き込み不可

バリデーションの実装例:

import "reflect-metadata";

function minLength(min: number) {
  return function (target: any, propertyKey: string) {
    Reflect.defineMetadata("minLength", min, target, propertyKey);
  };
}

function validate(obj: any): boolean {
  for (const key of Object.keys(obj)) {
    const min = Reflect.getMetadata("minLength", obj, key);
    if (min !== undefined && String(obj[key]).length < min) {
      console.error(`${key} は ${min}文字以上必要です`);
      return false;
    }
  }
  return true;
}

class User {
  @minLength(3)
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

const u = new User("AB");
validate(u); // name は 3文字以上必要です

パラメータデコレータ

パラメータデコレータはメソッドのパラメータ宣言の直前に置き、3つの引数を受け取ります。

引数 説明
target any クラスのプロトタイプ
propertyKey string | symbol メソッド名
parameterIndex number パラメータの位置(0始まり)
function required(target: any, propertyKey: string, parameterIndex: number) {
  const existingRequired: number[] =
    Reflect.getMetadata("required", target, propertyKey) || [];
  existingRequired.push(parameterIndex);
  Reflect.defineMetadata("required", existingRequired, target, propertyKey);
}

function validate(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const method = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const required: number[] =
      Reflect.getMetadata("required", target, propertyKey) || [];
    for (const idx of required) {
      if (args[idx] === undefined) {
        throw new Error(`引数 ${idx} は必須です`);
      }
    }
    return method.apply(this, args);
  };
}

class Greeter {
  @validate
  greet(@required name: string, greeting?: string) {
    return `${greeting ?? "Hello"}, ${name}!`;
  }
}

const g = new Greeter();
g.greet("Alice");           // "Hello, Alice!"
g.greet(undefined as any);  // Error: 引数 0 は必須です

デコレータファクトリ

デコレータファクトリは引数を受け取ってデコレータ関数を返す関数です。カスタマイズ可能なデコレータを作成する際に使います。

// デコレータファクトリ: 引数 → デコレータ関数
function retry(maxAttempts: number, delay: number = 1000) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const original = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          return await original.apply(this, args);
        } catch (err) {
          console.warn(`試行 ${attempt}/${maxAttempts} 失敗: ${err}`);
          if (attempt === maxAttempts) throw err;
          await new Promise(r => setTimeout(r, delay));
        }
      }
    };

    return descriptor;
  };
}

class ApiClient {
  @retry(3, 500)  // 最大3回、500ms間隔でリトライ
  async fetchUser(id: number) {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }
}
ファクトリのメリット
引数なしデコレータ(@log)は再利用できるが柔軟性に欠ける。ファクトリ(@retry(3, 500))にすれば設定値をその場で渡せて汎用的に使えます。

実行順序

複数のデコレータを適用する場合、書いた順(上から下)で評価され、適用は下から上(内側から外側)の順で行われます。

function first() {
  console.log("first(): evaluated");
  return function (target: any, key: string, desc: PropertyDescriptor) {
    console.log("first(): called");
  };
}

function second() {
  console.log("second(): evaluated");
  return function (target: any, key: string, desc: PropertyDescriptor) {
    console.log("second(): called");
  };
}

class Example {
  @first()
  @second()
  method() {}
}

// 出力:
// first(): evaluated   ← 上から評価
// second(): evaluated  ← 上から評価
// second(): called     ← 下から適用
// first(): called      ← 下から適用
デコレータの種類 実行タイミング
パラメータデコレータ メソッド/コンストラクタ内、上から順
メソッドデコレータ クラス内、上から順
プロパティデコレータ クラス内、上から順
クラスデコレータ すべてのメンバーデコレータの後

実務活用パターン

① パフォーマンス計測

function measure(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const original = descriptor.value;
  descriptor.value = async function (...args: any[]) {
    const start = performance.now();
    const result = await original.apply(this, args);
    const ms = (performance.now() - start).toFixed(2);
    console.log(`[${propertyKey}] ${ms}ms`);
    return result;
  };
}

class UserService {
  @measure
  async getUsers(): Promise<User[]> {
    return await db.query("SELECT * FROM users");
  }
}
// [getUsers] 42.31ms

② キャッシュ(メモ化)

function memoize(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const cache = new Map<string, any>();
  const original = descriptor.value;

  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log(`キャッシュヒット: ${propertyKey}(${key})`);
      return cache.get(key);
    }
    const result = original.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @memoize
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

③ NestJS スタイルの DI

NestJSは独自のDIコンテナを持ちますが、デコレータを使った簡易DIコンテナのイメージを示します:

const container = new Map<string, any>();

function Injectable() {
  return function (constructor: Function) {
    container.set(constructor.name, new (constructor as any)());
  };
}

function Inject(token: string) {
  return function (target: any, key: string) {
    Object.defineProperty(target, key, {
      get: () => container.get(token),
    });
  };
}

@Injectable()
class LogService {
  log(msg: string) { console.log(`[LOG] ${msg}`); }
}

class UserController {
  @Inject("LogService")
  private logger!: LogService;

  createUser(name: string) {
    this.logger.log(`ユーザー作成: ${name}`);
  }
}

const ctrl = new UserController();
ctrl.createUser("Alice"); // [LOG] ユーザー作成: Alice

TypeScript 5.0の新デコレータ(Stage 3仕様)

TypeScript 5.0(2023年3月)でECMAScript Stage 3デコレータが正式サポートされました。experimentalDecorators: true が不要で、型安全性も向上しています。

比較項目 experimentalDecorators TS5.0 Stage 3
tsconfig設定 experimentalDecorators: true が必要 不要(デフォルト有効)
型安全性 低い(any多用) 高い(型付きデコレータコンテキスト)
戻り値 descriptor を返す デコレータ関数を返す
ECMAScript標準 TypeScript独自 Stage 3(標準化進行中)
フレームワーク対応 Angular・NestJSで使用中 まだ移行途中
// TypeScript 5.0 Stage 3 デコレータの書き方
function logged<This, Args extends any[], Return>(
  target: (this: This, ...args: Args) => Return,
  context: ClassMethodDecoratorContext
) {
  const methodName = String(context.name);

  function replacement(this: This, ...args: Args): Return {
    console.log(`${methodName} を呼び出し`);
    const result = target.call(this, ...args);
    console.log(`${methodName} が完了`);
    return result;
  }

  return replacement;
}

class Person {
  name: string;
  constructor(name: string) { this.name = name; }

  @logged  // experimentalDecorators不要
  greet() {
    console.log(`Hello, I am ${this.name}`);
  }
}

const p = new Person("Alice");
p.greet();
// greet を呼び出し
// Hello, I am Alice
// greet が完了
移行の考え方
NestJS・Angularプロジェクトは当面 experimentalDecorators: true を継続。新規の素のTypeScriptプロジェクトならStage 3デコレータを検討する価値あり。両者は 共存できない(同一ファイル内での混在不可)ため注意。

よくあるエラーと対処法

エラー 原因 対処
Decorators are not valid here 変数宣言や関数式にデコレータを付けた クラス/メソッド/プロパティにのみ使用する
Unable to resolve signature of class decorator クラスデコレータの戻り値型が不一致 void か同じコンストラクタ型を返す
“experimentalDecorators” is not enabled tsconfig設定漏れ experimentalDecorators: true を追加
Cannot read properties of undefined (reflect-metadata) import漏れ エントリポイントで import "reflect-metadata"
型エラー: Property descriptor is undefined プロパティデコレータの誤用 メソッドデコレータで descriptor を使う
// BAD: 変数にデコレータは不可
@log
const x = 42; // エラー: Decorators are not valid here

// GOOD: クラスのメソッドに適用
class Foo {
  @log
  bar() {}
}

まとめ

デコレータ種別 引数 主な用途
クラスデコレータ constructor シール・拡張・DI登録
メソッドデコレータ target, key, descriptor ログ・リトライ・キャッシュ・認可
プロパティデコレータ target, key バリデーション・変換・readonly化
パラメータデコレータ target, key, index 必須チェック・メタデータ付与
アクセサデコレータ target, key, descriptor get/set のインターセプト

デコレータを理解することで、横断的関心事(ロギング・認証・キャッシュ)をビジネスロジックから分離でき、コードがシンプルで再利用性の高い構造になります。

関連記事:

FAQ

Qデコレータはランタイムに実行されますか?

Aはい。デコレータはクラス定義が評価されるタイミング(モジュール読み込み時)に実行されます。インスタンス生成のたびには実行されません。

Qデコレータとミックスイン(Mixin)の違いは?

Aミックスインはクラス継承で機能を合成する手法。デコレータは既存クラスに後付けで機能を付与します。用途に応じて使い分けますが、デコレータのほうが宣言的で読みやすいコードになります。

QReact でもデコレータを使えますか?

AReactそのものはデコレータを必須としませんが、MobXの @observable や一部状態管理ライブラリではデコレータを活用しています。experimentalDecorators: true を有効にすれば利用可能です。

Qメソッドデコレータの descriptor.value と descriptor.get の違いは?

Adescriptor.value は通常のメソッド用。アクセサ(get/set)には descriptor.get/descriptor.set を使います。両者を混同するとエラーになるため注意。

QTypeScript 5.0の新デコレータに移行すべきですか?

ANestJS・Angularなどの主要フレームワークは現在も experimentalDecorators ベースです。新規の小規模プロジェクトではStage 3の採用を検討できますが、フレームワークを使う場合は当面は従来仕様に従ってください。