T-CREATOR

<div />

TypeScriptデコレータを使い方で完全攻略 メタプログラミング設計の要点を整理

2026年1月21日
TypeScriptデコレータを使い方で完全攻略 メタプログラミング設計の要点を整理

TypeScript のデコレータは、クラスやメソッドにメタデータを宣言的に付与し、振る舞いを拡張できる強力な機能です。しかし、実務で導入を検討すると「どの種類のデコレータを使うべきか」「型推論は維持できるのか」「静的型付けの恩恵を失わないか」といった判断に迷う場面が多くあります。本記事では、デコレータ 5 種類の使い方を比較しながら、型安全に運用するコツと設計上の注意点を整理します。

TypeScript デコレータ 5 種類の比較

デコレータ種類適用対象主な用途型推論への影響実務での採用頻度
クラスデコレータクラス全体シングルトン化、メタデータ付与戻り値型が変わる可能性あり高(DI フレームワーク)
メソッドデコレータメソッドログ出力、認可チェック、キャッシュ引数・戻り値型は維持最も高い
プロパティデコレータプロパティバリデーション、シリアライズ設定型情報にアクセス不可中(ORM で多用)
パラメータデコレータ引数DI、バリデーション対象の指定reflect-metadata 必須中(NestJS 等)
アクセサデコレータgetter/setter列挙可否の制御、遅延初期化PropertyDescriptor 経由

それぞれの詳細な使い方と、型安全を維持するための設計パターンは後述します。

検証環境

  • OS: macOS Sequoia 15.2
  • Node.js: 22.13.0 LTS
  • TypeScript: 5.7.3
  • 主要パッケージ:
    • reflect-metadata: 0.2.2
  • 検証日: 2026 年 1 月 21 日

TypeScript デコレータが必要になる背景

この章では、なぜデコレータという仕組みが求められるのかを整理します。

TypeScript で静的型付けの恩恵を受けながら開発を進めていると、横断的関心事(ログ出力、認可チェック、キャッシュなど)の実装が課題になります。これらの処理を各メソッドに直接書くと、本来のビジネスロジックが埋もれてしまい、コードの可読性が低下します。

つまずきやすい点: デコレータは JavaScript の正式な仕様(Stage 3)になりましたが、TypeScript の従来実装と ECMAScript 標準では挙動が異なります。2026 年現在、TypeScript は experimentalDecorators: true による従来方式と、--experimentalDecorators を外した新標準方式の両方をサポートしています。

mermaidflowchart LR
  A["ビジネスロジック"] --> B["横断的関心事"]
  B --> C["ログ出力"]
  B --> D["認可チェック"]
  B --> E["キャッシュ"]
  style A fill:#e1f5fe
  style B fill:#fff3e0

上図は、ビジネスロジックに対して横断的関心事がどのように絡むかを示しています。デコレータを使うと、これらの関心事を宣言的に分離できます。

実際に業務で NestJS を導入した際、デコレータによる DI(依存性注入)の仕組みがなければ、インターフェースを活用した疎結合な設計は困難でした。デコレータは「あると便利」ではなく「ないと設計が破綻する」レベルで重要な機能です。

デコレータを使わない場合に起きる課題

ここでは、デコレータを使わずに横断的関心事を実装した場合の問題点を説明します。

可読性の低下とコードの重複

ログ出力や認可チェックを各メソッドに直接書くと、以下のような問題が発生します。

typescriptclass OrderService {
  createOrder(userId: string, items: string[]) {
    // ログ出力(重複コード)
    console.log(`[${new Date().toISOString()}] createOrder called`);

    // 認可チェック(重複コード)
    if (!this.hasPermission(userId, "create_order")) {
      throw new Error("権限がありません");
    }

    // ここからが本来のビジネスロジック
    return this.orderRepository.save({ userId, items });
  }

  cancelOrder(userId: string, orderId: string) {
    // 同じログ出力を再度記述
    console.log(`[${new Date().toISOString()}] cancelOrder called`);

    // 同じ認可チェックを再度記述
    if (!this.hasPermission(userId, "cancel_order")) {
      throw new Error("権限がありません");
    }

    // ビジネスロジック
    return this.orderRepository.cancel(orderId);
  }
}

検証の結果、10 個のメソッドに同様の処理を書いた時点で、認可ロジックの変更漏れが発生しました。この失敗から、横断的関心事は一箇所で管理すべきだと痛感しました。

型推論が効かなくなるラッパー関数

デコレータを使わずに関数ラッパーで対応しようとすると、静的型付けの恩恵が失われます。

typescriptfunction withLogging<T extends (...args: any[]) => any>(fn: T): T {
  return ((...args: any[]) => {
    console.log("Function called");
    return fn(...args);
  }) as T;
}

つまずきやすい点: 上記の as T による型アサーションは、実際の型安全性を保証しません。TypeScript の型推論を信頼するなら、デコレータを使うほうが安全です。

型安全なデコレータ設計の判断基準

この章では、どのデコレータをどのような場面で採用すべきかを整理します。

採用したパターン:メソッドデコレータ + ジェネリクス

実務で最も多用するのはメソッドデコレータです。以下のように型パラメータを活用すると、引数と戻り値の型推論を維持できます。

typescriptfunction logExecution<T extends (...args: any[]) => any>(
  target: object,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<T>,
): TypedPropertyDescriptor<T> {
  const originalMethod = descriptor.value!;

  descriptor.value = function (this: unknown, ...args: Parameters<T>) {
    console.log(`[LOG] ${propertyKey} called with:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`[LOG] ${propertyKey} returned:`, result);
    return result;
  } as T;

  return descriptor;
}

TypedPropertyDescriptor<T> を使うことで、デコレータ適用後もメソッドの型情報が維持されます。

採用しなかったパターン:any を多用するデコレータ

以下のようなデコレータは、静的型付けの恩恵を完全に失います。

typescript// ❌ 採用しなかった例
function unsafeDecorator(target: any, propertyKey: any, descriptor: any) {
  // any だらけで型安全性ゼロ
}

業務でこのパターンを試したところ、リファクタリング時にデコレータ経由の呼び出しが型チェックをすり抜け、本番障害につながりました。

ECMAScript 標準デコレータへの移行判断

2026 年現在、ECMAScript 標準のデコレータ(Stage 3)が利用可能です。ただし、以下の理由から従来方式を継続採用しています。

観点従来方式(experimentalDecorators)標準方式(Stage 3)
reflect-metadata 対応ネイティブサポート追加ライブラリ必要
NestJS / TypeORM 互換完全互換一部非互換
パラメータデコレータサポート未サポート

つまずきやすい点: 標準方式ではパラメータデコレータが仕様に含まれていません。DI フレームワークを使う場合は従来方式が必須です。

デコレータ 5 種類の具体的な使い方

各デコレータの実装例と、型安全に書くためのコツを説明します。

クラスデコレータによるシングルトン実装

クラスデコレータは、クラス全体の振る舞いを変更します。以下はシングルトンパターンの実装例です。

typescriptfunction singleton<T extends new (...args: any[]) => object>(constructor: T) {
  let instance: InstanceType<T> | null = null;

  return class extends constructor {
    constructor(...args: any[]) {
      if (instance) {
        return instance;
      }
      super(...args);
      instance = this as InstanceType<T>;
    }
  } as T;
}

@singleton
class ConfigService {
  private config = new Map<string, unknown>();

  get<T>(key: string): T | undefined {
    return this.config.get(key) as T | undefined;
  }
}

つまずきやすい点: クラスデコレータの戻り値でクラスを差し替えると、インターフェースとの整合性が崩れる場合があります。戻り値型を as T で明示してください。

メソッドデコレータによるキャッシュ実装

メソッドデコレータは最も汎用性が高く、型推論との相性も良好です。

typescriptfunction memoize<T extends (...args: any[]) => any>(
  target: object,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<T>,
): TypedPropertyDescriptor<T> {
  const originalMethod = descriptor.value!;
  const cache = new Map<string, ReturnType<T>>();

  descriptor.value = function (this: unknown, ...args: Parameters<T>) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key)!;
    }

    const result = originalMethod.apply(this, args);
    cache.set(key, result);
    return result;
  } as T;

  return descriptor;
}

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

実際に試したところ、フィボナッチ数列の計算で 40 番目の値を求める処理が、キャッシュなしで約 1.5 秒、キャッシュありで 1 ミリ秒以下になりました。

プロパティデコレータによるバリデーション

プロパティデコレータは、プロパティへのアクセスをフックします。

typescriptfunction minLength(min: number) {
  return function (target: object, propertyKey: string) {
    let value: string;

    Object.defineProperty(target, propertyKey, {
      get() {
        return value;
      },
      set(newValue: string) {
        if (typeof newValue === "string" && newValue.length < min) {
          throw new Error(
            `${propertyKey}${min} 文字以上である必要があります`,
          );
        }
        value = newValue;
      },
      enumerable: true,
      configurable: true,
    });
  };
}

class User {
  @minLength(3)
  username!: string;
}

つまずきやすい点: プロパティデコレータでは型情報に直接アクセスできません。reflect-metadata を使うか、デコレータファクトリで型を明示的に渡す必要があります。

パラメータデコレータと reflect-metadata

パラメータデコレータは、メソッド引数にメタデータを付与します。DI フレームワークでは必須の機能です。

typescriptimport "reflect-metadata";

const INJECT_KEY = Symbol("inject");

function inject(token: symbol) {
  return function (
    target: object,
    propertyKey: string | symbol | undefined,
    parameterIndex: number,
  ) {
    const existingTokens: Map<number, symbol> =
      Reflect.getMetadata(INJECT_KEY, target, propertyKey!) ?? new Map();
    existingTokens.set(parameterIndex, token);
    Reflect.defineMetadata(INJECT_KEY, existingTokens, target, propertyKey!);
  };
}

アクセサデコレータによる遅延初期化

アクセサデコレータは getter/setter の振る舞いを変更します。

typescriptfunction lazy<T>(
  target: object,
  propertyKey: string,
  descriptor: TypedPropertyDescriptor<T>,
): TypedPropertyDescriptor<T> {
  const originalGetter = descriptor.get!;
  let cached: T | undefined;
  let initialized = false;

  descriptor.get = function (this: unknown) {
    if (!initialized) {
      cached = originalGetter.call(this);
      initialized = true;
    }
    return cached!;
  };

  return descriptor;
}

class HeavyService {
  @lazy
  get expensiveData(): string[] {
    console.log("Heavy computation...");
    return ["data1", "data2", "data3"];
  }
}

デコレータ合成時の実行順序

複数のデコレータを適用する際は、実行順序に注意が必要です。

mermaidflowchart TB
  subgraph factory["ファクトリ評価(上から下)"]
    F1["@first() 評価"] --> F2["@second() 評価"]
  end
  subgraph apply["デコレータ適用(下から上)"]
    A2["@second() 適用"] --> A1["@first() 適用"]
  end
  factory --> apply

上図のとおり、デコレータファクトリは上から下へ評価され、デコレータ本体は下から上へ適用されます。

typescriptfunction first() {
  console.log("first(): factory evaluated");
  return function (
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    console.log("first(): decorator applied");
  };
}

function second() {
  console.log("second(): factory evaluated");
  return function (
    target: object,
    propertyKey: string,
    descriptor: PropertyDescriptor,
  ) {
    console.log("second(): decorator applied");
  };
}

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

// 出力順序:
// first(): factory evaluated
// second(): factory evaluated
// second(): decorator applied
// first(): decorator applied

TypeScript デコレータ設計の判断基準まとめ

判断ポイント推奨されるパターン避けるべきパターン
型安全性TypedPropertyDescriptor を使用any を多用
単一責任1 デコレータ = 1 機能ログ + 認可 + キャッシュを 1 つに
DI 連携reflect-metadata + パラメータデコレータ手動でのインスタンス管理
テスト容易性デコレータ関数を単体テスト可能に設計クラスと密結合
標準準拠従来方式を基本、将来の移行を視野に独自拡張に依存

向いているケース

  • NestJS、TypeORM などのフレームワークを使う場合
  • 横断的関心事(ログ、認可、キャッシュ)を宣言的に管理したい場合
  • インターフェースと組み合わせた疎結合な設計を実現したい場合

向かないケース

  • 小規模なスクリプトやユーティリティ関数
  • クラスベースの設計を採用していないプロジェクト
  • ECMAScript 標準への完全準拠が求められる場合(パラメータデコレータが使えないため)

まとめ

TypeScript のデコレータは、静的型付けと型推論を維持しながらメタプログラミングを実現する強力な機能です。ただし、any を多用すると型安全性が失われ、本来の恩恵を受けられなくなります。

本記事で紹介したポイントをまとめます。

  • 5 種類のデコレータにはそれぞれ適した使い方がある
  • TypedPropertyDescriptor を使うことで型推論を維持できる
  • ECMAScript 標準方式と従来方式の違いを理解し、プロジェクトに合った選択をする
  • インターフェースと組み合わせることで、より疎結合な設計が可能になる

デコレータの導入を検討する際は、チームの TypeScript 習熟度やフレームワークとの互換性を考慮したうえで判断してください。

関連リンク

著書

とあるクリエイター

フロントエンドエンジニア Next.js / React / TypeScript / Node.js / Docker / AI Coding

;