TypeScript 速習チュートリアル

TypeScript のデコレータ

デコレータ(Decorators)は、設計時にクラスやそのメンバーに対してメタデータを追加したり、振る舞いを修正したりできる TypeScript の強力な機能です。
Angular や NestJS といったフレームワークでは、依存性の注入(Dependency Injection)、ルーティング、バリデーションなどに広く利用されています。

1. デコレータの有効化

TypeScript でデコレータを使用するには、tsconfig.json で機能を有効にする必要があります。

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "strictPropertyInitialization": false
  },
  "include": ["src/**/*.ts"]
}

       注意:emitDecoratorMetadata オプションは、デコレータの型メタデータを出力するための実験的サポートを有効にします。これは TypeORM や class-validator などのライブラリで使用されます。

2. デコレータの種類

TypeScript は、異なる宣言に適用できる数種類のデコレータをサポートしています。

デコレータの種類適用対象シグネチャ(Signature)
クラスデコレータクラス宣言(constructor: Function) => void
メソッドデコレータクラスメソッド(target: any, propertyKey: string, descriptor: PropertyDescriptor) => void
プロパティデコレータクラスプロパティ(target: any, propertyKey: string) => void
パラメータデコレータメソッドの引数(target: any, propertyKey: string, parameterIndex: number) => void

3. クラスデコレータ

クラスデコレータはクラスのコンストラクタに適用され、クラス定義の監視、修正、または置換に使用されます。
これらはインスタンスが作成されたときではなく、クラスが宣言されたときに呼び出されます。

3.1 基本的なクラスデコレータ

このシンプルなデコレータは、クラスが定義されたタイミングをログに記録します。

コード例

// クラス定義をログ出力するシンプルなクラスデコレータ
function logClass(constructor: Function) {
  console.log(`クラス ${constructor.name} が ${new Date().toISOString()} に定義されました`);
}

// デコレータの適用
@logClass
class UserService {
  getUsers() {
    return ['Alice', 'Bob', 'Charlie'];
  }
}

// ファイルロード時の出力: "クラス UserService が [timestamp] に定義されました"

3.2 コンストラクタを修正するクラスデコレータ

プロパティやメソッドを追加してクラスを修正する例です。

コード例

// バージョンプロパティを追加し、インスタンス化をログ出力するデコレータ
function versioned(version: string) {
  return function (constructor: Function) {
    // 静的プロパティを追加
    constructor.prototype.version = version;
    
    // 元のコンストラクタを保存
    const original = constructor;
    // 元のコンストラクタをラップする新しいコンストラクタを作成
    const newConstructor: any = function (...args: any[]) {
      console.log(`${original.name} v${version} のインスタンスを作成中`);
      return new original(...args);
    };
    
    // instanceof が動作するようにプロトタイプをコピー
    newConstructor.prototype = original.prototype;
    return newConstructor;
  };
}

// バージョンを指定してデコレータを適用
@versioned('1.0.0')
class ApiClient {
  fetchData() {
    console.log('データを取得中...');
  }
}

const client = new ApiClient();
console.log((client as any).version); // 出力: 1.0.0
client.fetchData();

3.3 Sealed(封印)クラスデコレータ

クラスに新しいプロパティを追加できないようにし、既存のプロパティを構成不可(non-configurable)にします。

コード例

function sealed(constructor: Function) {
  console.log(`${constructor.name} を封印中...`);
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return `こんにちは、${this.greeting}`;
  }
}

// ストリクトモードではエラーになります
// Greeter.prototype.newMethod = function() {}; // エラー: newMethod プロパティを追加できません

4. メソッドデコレータ

メソッドデコレータはメソッド定義に適用され、メソッド定義の監視、修正、置換が可能です。
3つのパラメータを受け取ります:

  1. target: クラスのプロトタイプ(インスタンスメソッドの場合)またはコンストラクタ関数(静的メソッドの場合)。
  2. propertyKey: メソッドの名前。
  3. descriptor: メソッドのプロパティディスクリプタ(Property Descriptor)。

4.1 メソッド実行時間計測デコレータ

メソッドの実行時間を計測し、ログに出力します。

コード例

// 実行時間を計測するメソッドデコレータ
function measureTime(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const start = performance.now();
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`${propertyKey} の実行時間: ${(end - start).toFixed(2)}ms`);
    return result;
  };
  return descriptor;
}

class DataProcessor {
  @measureTime
  processData(data: number[]): number[] {
    // 処理時間をシミュレート
    for (let i = 0; i < 100000000; i++) { /* 処理 */ }
    return data.map(x => x * 2);
  }
}

const processor = new DataProcessor();
processor.processData([1, 2, 3, 4, 5]);

4.2 メソッド認可(Authorization)デコレータ

ロールベースのアクセス制御(RBAC)を実装する例です。

コード例

type UserRole = 'admin' | 'editor' | 'viewer';

const currentUser = {
  id: 1,
  name: 'John Doe',
  roles: ['viewer'] as UserRole[]
};

function AllowedRoles(...allowedRoles: UserRole[]) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const hasPermission = allowedRoles.some(role =>
        currentUser.roles.includes(role)
      );
      if (!hasPermission) {
        throw new Error(
          `ユーザー ${currentUser.name} には ${propertyKey} を呼び出す権限がありません`
        );
      }
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

class DocumentService {
  @AllowedRoles('admin', 'editor')
  deleteDocument(id: string) {
    console.log(`ドキュメント ${id} を削除しました`);
  }
  @AllowedRoles('admin', 'editor', 'viewer')
  viewDocument(id: string) {
    console.log(`ドキュメント ${id} を表示中`);
  }
}

const docService = new DocumentService();
try {
  docService.viewDocument('doc123'); // 成功 - viewer ロールは許可されています
  docService.deleteDocument('doc123'); // エラー - viewer は削除できません
} catch (error: any) {
  console.error(error.message);
}

5. プロパティデコレータ

プロパティデコレータはプロパティ宣言に適用されます。パラメータとして targetpropertyKey を受け取ります。

5.1 フォーマット済みプロパティデコレータ

値がセットされたときに自動的にフォーマットを適用します。

コード例

function format(formatString: string) {
  return function (target: any, propertyKey: string) {
    let value: string;
    const getter = () => value;
    const setter = (newVal: string) => {
      value = formatString.replace('{}', newVal);
    };
    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
    });
  };
}

class Greeter {
  @format('こんにちは、{}!')
  greeting: string;
}

const greeter = new Greeter();
greeter.greeting = '世界';
console.log(greeter.greeting); // 出力: こんにちは、世界!

6. パラメータデコレータ

メソッドの引数に対して適用されます。主にメタデータの保存に使用され、メソッドデコレータと組み合わせてバリデーションなどを実装します。

コード例

import "reflect-metadata";

function validateParam(type: 'string' | 'number' | 'boolean') {
  return function (target: any, propertyKey: string | symbol, parameterIndex: number) {
    const existingValidations: any[] = Reflect.getOwnMetadata('validations', target, propertyKey) || [];
    existingValidations.push({ index: parameterIndex, type });
    Reflect.defineMetadata('validations', existingValidations, target, propertyKey);
  };
}

function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    const validations: Array<{index: number, type: string}> =
      Reflect.getOwnMetadata('validations', target, propertyKey) || [];

    for (const validation of validations) {
      const { index, type } = validation;
      const param = args[index];
      let isValid = false;

      switch (type) {
        case 'string':
          isValid = typeof param === 'string' && param.length > 0;
          break;
        case 'number':
          isValid = typeof param === 'number' && !isNaN(param);
          break;
        case 'boolean':
          isValid = typeof param === 'boolean';
      }

      if (!isValid) {
        throw new Error(`インデックス ${index} のパラメータが ${type} のバリデーションに失敗しました`);
      }
    }

    return originalMethod.apply(this, args);
  };
  return descriptor;
}

class UserService {
  @validate
  createUser(
    @validateParam('string') name: string,
    @validateParam('number') age: number,
    @validateParam('boolean') isActive: boolean
  ) {
    console.log(`ユーザー作成: ${name}, ${age}, ${isActive}`);
  }
}

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

デコレータファクトリは、デコレータ関数を返す関数です。これにより、引数を渡してデコレータの動作をカスタマイズできるようになり、再利用性が高まります。

コード例

function logWithConfig(config: { level: 'log' | 'warn' | 'error', message?: string }) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const { level = 'log', message = 'メソッドを実行中' } = config;
      console[level](`${message}: ${propertyKey}`, { arguments: args });
      const result = originalMethod.apply(this, args);
      console[level](`${propertyKey} が完了しました`);
      return result;
    };
    return descriptor;
  };
}

8. 評価の順序

複数のデコレータが適用されている場合、以下の順序で評価・適用されます。

  1. インスタンスメンバーに対して、パラメータデコレータ、続いてメソッド/プロパティデコレータ。
  2. 静的メンバーに対して、パラメータデコレータ、続いてメソッド/プロパティデコレータ。
  3. コンストラクタに対して、パラメータデコレータ。
  4. クラスに対して、クラスデコレータ。

デコレータ関数自体の実行は「下から上(内側から外側)」に向かって行われます。

9. 実践的な例:API コントローラー

NestJS のようなスタイルで、デコレータを使用してルーティングを定義する例です。

コード例

const ROUTES: any[] = [];

function Controller(prefix: string = '') {
  return function (constructor: Function) {
    constructor.prototype.prefix = prefix;
  };
}

function Get(path: string = '') {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    ROUTES.push({
      method: 'get',
      path,
      handler: descriptor.value,
      target: target.constructor
    });
  };
}

@Controller('/users')
class UserController {
  @Get('/')
  getAllUsers() {
    return { users: [{ id: 1, name: 'John' }] };
  }

  @Get('/:id')
  getUserById(id: string) {
    return { id, name: 'John' };
  }
}

function registerRoutes() {
  ROUTES.forEach(route => {
    const prefix = route.target.prototype.prefix || '';
    console.log(`Registered: ${route.method.toUpperCase()} ${prefix}${route.path}`);
  });
}

registerRoutes();
// 出力:
// Registered: GET /users/
// Registered: GET /users/:id

10. ベストプラクティス

  • 責務を明確にする: 一つのデコレータには一つの役割だけを持たせます。
  • ドキュメント化: デコレータがどのような副作用を持つかを明示します。
  • ファクトリの活用: 設定可能なデコレータを作成し、汎用性を高めます。
  • パフォーマンスへの配慮: ランタイムのオーバーヘッドを意識し、クリティカルな箇所での過度な使用は避けます。
  • reflect-metadata の利用: 高度な実行時型情報が必要な場合に検討します。

11. よくある落とし穴

  • 有効化の忘れ: tsconfig.jsonexperimentalDecorators を確認してください。
  • シグネチャの誤り: 各デコレータタイプには固有の引数があります。
  • 評価順序の誤解: デコレータは記述順の下から上に適用される点に注意してください。
  • プロパティの初期化: プロパティデコレータは、インスタンスのプロパティが初期化される前に実行されます。
  • ブラウザ互換性: 古いブラウザではトランスパイルが必要になる場合があります。