【TypeScript】デコレータ実践 - メタプログラミング入門

【TypeScript】デコレータ実践 - メタプログラミング入門

2024-11-10

2024-11-10

デコレータ実践:メタプログラミング入門

TypeScriptのデコレータは、クラスやメソッド、プロパティに対して追加の機能を付与できる強力なメタプログラミングの手法です。デコレータを使うことで、コードに付加的な機能を簡潔に組み込み、再利用性の高い設計を実現できます。本記事では、TypeScriptのデコレータの概要と使い方、実践的な使用例を紹介します。

TypeScriptのデコレータとは?

デコレータは、クラスやメソッド、プロパティに特定の装飾を加える関数です。デコレータを使うことで、クラスの定義部分に装飾的な処理を追加でき、コードの拡張性や保守性が向上します。TypeScriptのデコレータ機能はECMAScriptの提案仕様に基づいており、TypeScript独自の構文で実装されています。

デコレータの種類

TypeScriptでは、以下の4種類のデコレータが利用できます。

  1. クラスデコレータ: クラス全体に対する装飾を行います。
  2. メソッドデコレータ: メソッドに対して装飾を行います。
  3. アクセサデコレータ: プロパティのゲッターやセッターに装飾を行います。
  4. プロパティデコレータ: 特定のプロパティに装飾を行います。

デコレータの基本的な使用方法

デコレータを使うには、experimentalDecoratorsオプションを有効にする必要があります。tsconfig.jsonに以下の設定を追加しましょう。

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}

それでは、各デコレータの具体的な使い方を見ていきましょう。

クラスデコレータ

クラスデコレータは、クラスの定義時に呼び出され、クラス全体に対する装飾を行います。例えば、インスタンスにタイムスタンプを追加するクラスデコレータを作成できます。

function Timestamped<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    timestamp = new Date();
  };
}
@Timestamped
class User {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
const user = new User("Alice");
console.log(user); // User { name: "Alice", timestamp: ... }

この例では、@Timestampedデコレータにより、クラスにタイムスタンププロパティが追加されています。

メソッドデコレータ

メソッドデコレータは、メソッドに対して付加的な処理を行います。たとえば、メソッドの実行時間を計測する@logExecutionTimeデコレータを作成します。

function logExecutionTime(
  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} took ${end - start} ms`);
    return result;
  };
}
class Calculator {
  @logExecutionTime
  add(a: number, b: number): number {
    return a + b;
  }
}
const calculator = new Calculator();
calculator.add(5, 10); // "add took XX ms" と表示

この例では、@logExecutionTimeデコレータがaddメソッドに適用され、実行時間が自動的にログに記録されます。

アクセサデコレータ

アクセサデコレータは、ゲッターやセッターに対して装飾を行います。次の例では、プロパティの値を監視するためのアクセサデコレータを作成しています。

function Watch(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalSet = descriptor.set;
  descriptor.set = function (value: any) {
    console.log(`Setting ${propertyKey} to ${value}`);
    if (originalSet) {
      originalSet.call(this, value);
    }
  };
}
class Person {
  private _name: string = "";
  @Watch
  set name(value: string) {
    this._name = value;
  }
  get name(): string {
    return this._name;
  }
}
const person = new Person();
person.name = "Alice"; // "Setting name to Alice" と表示

この例では、@Watchデコレータにより、プロパティの値が変更される際にログが記録されます。

プロパティデコレータ

プロパティデコレータは、特定のプロパティに対して装飾を行います。たとえば、プロパティを読み取り専用にするデコレータを作成できます。

function Readonly(target: any, propertyKey: string) {
  Object.defineProperty(target, propertyKey, {
    writable: false
  });
}
class Car {
  @Readonly
  brand: string = "Toyota";
}
const car = new Car();
console.log(car.brand); // "Toyota"
// car.brand = "Honda"; // エラー: brandは書き込み禁止

この例では、@Readonlyデコレータをbrandプロパティに適用することで、brandプロパティが読み取り専用になります。

実践的なデコレータの使用例

デコレータは実際のプロジェクトで役立つ場面が多くあります。以下に、実践的なデコレータの使用例を紹介します。

権限チェックのためのデコレータ

認証や権限に基づいて、メソッドの実行可否をチェックするデコレータを作成できます。

function Authorized(role: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      const userRole = "admin"; // 例としてadminを設定
      if (userRole !== role) {
        throw new Error("You do not have permission to execute this method");
      }
      return originalMethod.apply(this, args);
    };
  };
}
class AdminPanel {
  @Authorized("admin")
  deleteUser(userId: number) {
    console.log(`User ${userId} deleted`);
  }
}
const panel = new AdminPanel();
try {
  panel.deleteUser(1); // "User 1 deleted" と表示
} catch (error) {
  console.error(error.message);
}

この例では、 @Authorized("admin")デコレータにより、管理者権限を持つユーザーのみdeleteUserメソッドが実行可能になります。

キャッシュ処理を行うデコレータ

結果が変わらない計算結果などをキャッシュするデコレータを作成することで、処理の効率を向上できます。

function Cache(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const cache = new Map<string, any>();
  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;
  };
}
class Calculator {
  @Cache
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}
const calculator = new Calculator();
console.log(calculator.fibonacci(10)); // 計算結果がキャッシュされる

この@Cacheデコレータにより、fibonacciメソッドの計算結果がキャッシュされ、同じ引数での再計算が省略されます。

デコレータのメリット

TypeScriptのデコレータを活用すると、以下のメリットが得られます。

  1. コードの再利用性向上
    デコレータを使うことで、共通処理を簡潔に書き、複数のクラスやメソッドで再利用可能です。
  2. 機能の追加が簡単
    コードを直接変更せずにデコレータを追加するだけで、新たな機能を付加できます。
  3. メタプログラミングによる柔軟な設計
    デコレータにより、プログラムの構造に基づいた処理が可能になり、柔軟なコード設計が実現します。

まとめ

TypeScriptのデコレータは、クラスやメソッドに装飾的な機能を追加し、再利用性と保守性の高いコードを実現するための強力なツールです。クラスデコレータやメソッドデコレータなどを適切に活用し、プロジェクトのニーズに応じた柔軟で型安全なコード設計を目指しましょう。

Recommend