T-CREATOR

typeScript デコレータ完全攻略:メタプログラミングの威力を解放する

typeScript デコレータ完全攻略:メタプログラミングの威力を解放する

TypeScript のデコレータは、クラス、メソッド、プロパティなどのメタデータを宣言的に変更・拡張できる強力な機能です。この記事では、デコレータの基礎から実践的な活用方法まで、メタプログラミングの可能性を探ります。

デコレータとは:メタプログラミングの基礎

デコレータは、クラスやそのメンバーの動作を変更・拡張するための特別な宣言です。Java や Python のアノテーションに似た機能で、コードの振る舞いを宣言的に定義できます。

typescript// デコレータの基本的な例
function log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(
      `メソッド ${propertyKey} が呼び出されました`
    );
    return originalMethod.apply(this, args);
  };

  return descriptor;
}

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

デコレータの種類と基本的な使い方

クラスデコレータ

クラス全体の振る舞いを変更します:

typescriptfunction singleton<T extends { new (...args: any[]): {} }>(
  constructor: T
) {
  let instance: T;

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

@singleton
class ConfigService {
  private config: Record<string, any> = {};

  setConfig(key: string, value: any) {
    this.config[key] = value;
  }
}

メソッドデコレータ

メソッドの振る舞いを拡張または変更します:

typescriptfunction measureTime(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    const start = performance.now();
    const result = await originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`実行時間: ${end - start}ms`);
    return result;
  };

  return descriptor;
}

class ApiService {
  @measureTime
  async fetchData() {
    // データ取得の処理
  }
}

プロパティデコレータ

プロパティの定義を変更または拡張します:

typescriptfunction required(target: any, propertyKey: string) {
  let value: any;

  const getter = function () {
    return value;
  };

  const setter = function (newVal: any) {
    if (newVal === undefined || newVal === null) {
      throw new Error(`${propertyKey}は必須です`);
    }
    value = newVal;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

class User {
  @required
  name: string;
}

パラメータデコレータ

メソッドパラメータに関する情報を提供します:

typescriptfunction validate(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  const existingValidators =
    Reflect.getMetadata(
      'validators',
      target,
      propertyKey
    ) || [];
  existingValidators.push(parameterIndex);
  Reflect.defineMetadata(
    'validators',
    existingValidators,
    target,
    propertyKey
  );
}

class UserService {
  createUser(
    @validate name: string,
    @validate email: string
  ) {
    // ユーザー作成処理
  }
}

アクセサデコレータ

ゲッターやセッターの振る舞いを変更します:

typescriptfunction enumerable(value: boolean) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    descriptor.enumerable = value;
    return descriptor;
  };
}

class Person {
  private _name: string;

  @enumerable(false)
  get name() {
    return this._name;
  }
}

デコレータファクトリの活用

デコレータファクトリを使用すると、パラメータ化されたデコレータを作成できます:

typescriptfunction timeout(milliseconds: number) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      return new Promise((resolve, reject) => {
        const timeoutId = setTimeout(() => {
          reject(new Error('タイムアウトしました'));
        }, milliseconds);

        Promise.resolve(originalMethod.apply(this, args))
          .then((result) => {
            clearTimeout(timeoutId);
            resolve(result);
          })
          .catch(reject);
      });
    };

    return descriptor;
  };
}

class ApiClient {
  @timeout(5000)
  async fetchData() {
    // データ取得処理
  }
}

実践的なデコレータパターン

バリデーションデコレータ

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

    const getter = function () {
      return value;
    };

    const setter = function (newVal: string) {
      if (newVal.length < min) {
        throw new Error(
          `${propertyKey}${min}文字以上である必要があります`
        );
      }
      value = newVal;
    };

    Object.defineProperty(target, propertyKey, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true,
    });
  };
}

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

ログ出力デコレータ

typescriptfunction logMethod(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(
      `[${new Date().toISOString()}] ${propertyKey}が呼び出されました`
    );
    console.log('引数:', args);

    const result = originalMethod.apply(this, args);

    console.log('戻り値:', result);
    return result;
  };

  return descriptor;
}

class OrderService {
  @logMethod
  createOrder(userId: string, items: string[]) {
    // 注文作成処理
  }
}

キャッシュデコレータ

typescriptfunction memoize(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  const cache = new Map();

  descriptor.value = function (...args: any[]) {
    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;
  };

  return descriptor;
}

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

認可デコレータ

typescriptfunction authorize(roles: string[]) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const currentUser = getCurrentUser(); // 現在のユーザー情報を取得

      if (
        !roles.some((role) =>
          currentUser.roles.includes(role)
        )
      ) {
        throw new Error('権限がありません');
      }

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

    return descriptor;
  };
}

class AdminPanel {
  @authorize(['admin'])
  deleteUser(userId: string) {
    // ユーザー削除処理
  }
}

エラーハンドリングデコレータ

typescriptfunction handleErrors(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = async function (...args: any[]) {
    try {
      return await originalMethod.apply(this, args);
    } catch (error) {
      console.error(
        `エラーが発生しました: ${error.message}`
      );
      // エラー通知やログ記録などの処理
      throw error;
    }
  };

  return descriptor;
}

class DataService {
  @handleErrors
  async fetchUserData(userId: string) {
    // データ取得処理
  }
}

デコレータの合成とチェーン

複数のデコレータを組み合わせて使用できます:

typescriptfunction first() {
  console.log('first(): factory evaluated');
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log('first(): called');
  };
}

function second() {
  console.log('second(): factory evaluated');
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log('second(): called');
  };
}

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

デコレータのテスト手法

デコレータのテストは、振る舞いの検証に焦点を当てます:

typescriptdescribe('LogDecorator', () => {
  it('should log method calls', () => {
    const consoleSpy = jest.spyOn(console, 'log');

    class TestClass {
      @logMethod
      testMethod(arg: string) {
        return `Result: ${arg}`;
      }
    }

    const instance = new TestClass();
    instance.testMethod('test');

    expect(consoleSpy).toHaveBeenCalled();
    expect(consoleSpy).toHaveBeenCalledWith(
      expect.stringContaining('testMethod'),
      expect.any(Array)
    );
  });
});

パフォーマンスとデバッグ

デコレータのパフォーマンスを最適化するためのベストプラクティス:

  1. キャッシュの活用
  2. 不要なメタデータの最小化
  3. 非同期処理の適切な管理
  4. メモリリークの防止
typescriptfunction optimizedDecorator(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  const cache = new WeakMap();

  descriptor.value = function (...args: any[]) {
    if (!cache.has(this)) {
      cache.set(this, new Map());
    }

    const instanceCache = cache.get(this);
    const key = JSON.stringify(args);

    if (instanceCache.has(key)) {
      return instanceCache.get(key);
    }

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

  return descriptor;
}

デコレータのベストプラクティス

  1. 単一責任の原則を守る
  2. デコレータの再利用性を高める
  3. 適切なエラーハンドリング
  4. 型安全性の確保
  5. テスト容易性の考慮
typescript// 良い例:単一責任の原則を守ったデコレータ
function validate(schema: any) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const validationResult = schema.validate(args[0]);
      if (validationResult.error) {
        throw new ValidationError(validationResult.error);
      }
      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

// 悪い例:複数の責任を持つデコレータ
function validateAndLog(schema: any) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      // バリデーション
      const validationResult = schema.validate(args[0]);
      if (validationResult.error) {
        throw new ValidationError(validationResult.error);
      }

      // ログ出力
      console.log(
        `Method ${propertyKey} called with args:`,
        args
      );

      // キャッシュ
      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;
    };

    return descriptor;
  };
}

まとめ:効果的なデコレータ設計のポイント

  1. 明確な目的: 各デコレータは明確な単一の目的を持つべき
  2. 再利用性: 汎用的に使えるよう設計する
  3. 型安全性: TypeScript の型システムを最大限活用する
  4. パフォーマンス: 必要な場合のみメタデータを使用し、キャッシュを活用する
  5. テスト容易性: デコレータの振る舞いを単体でテストできるようにする
  6. エラーハンドリング: 適切なエラー処理と例外処理を実装する
  7. ドキュメント: デコレータの使用方法と制限事項を明確に文書化する

デコレータは、TypeScript の強力なメタプログラミング機能の一つです。適切に使用することで、コードの可読性、保守性、再利用性を大きく向上させることができます。

関連リンク