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)
);
});
});
パフォーマンスとデバッグ
デコレータのパフォーマンスを最適化するためのベストプラクティス:
- キャッシュの活用
- 不要なメタデータの最小化
- 非同期処理の適切な管理
- メモリリークの防止
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;
}
デコレータのベストプラクティス
- 単一責任の原則を守る
- デコレータの再利用性を高める
- 適切なエラーハンドリング
- 型安全性の確保
- テスト容易性の考慮
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;
};
}
まとめ:効果的なデコレータ設計のポイント
- 明確な目的: 各デコレータは明確な単一の目的を持つべき
- 再利用性: 汎用的に使えるよう設計する
- 型安全性: TypeScript の型システムを最大限活用する
- パフォーマンス: 必要な場合のみメタデータを使用し、キャッシュを活用する
- テスト容易性: デコレータの振る舞いを単体でテストできるようにする
- エラーハンドリング: 適切なエラー処理と例外処理を実装する
- ドキュメント: デコレータの使用方法と制限事項を明確に文書化する
デコレータは、TypeScript の強力なメタプログラミング機能の一つです。適切に使用することで、コードの可読性、保守性、再利用性を大きく向上させることができます。
関連リンク
- blog
「アジャイルコーチ」って何する人?チームを最強にする影の立役者の役割と、あなたがコーチになるための道筋
- blog
ペアプロって本当に効果ある?メリットだけじゃない、現場で感じたリアルな課題と乗り越え方
- blog
TDDって結局何がいいの?コードに自信が持てる、テスト駆動開発のはじめの一歩
- blog
「昨日やったこと、今日やること」の報告会じゃない!デイリースクラムをチームのエンジンにするための3つの問いかけ
- blog
燃え尽きるのは誰だ?バーンダウンチャートでプロジェクトの「ヤバさ」をチームで共有する方法
- blog
「誰が、何を、なぜ」が伝わらないユーザーストーリーは無意味。開発者が本当に欲しいストーリーの書き方