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つのパラメータを受け取ります:
- target: クラスのプロトタイプ(インスタンスメソッドの場合)またはコンストラクタ関数(静的メソッドの場合)。
- propertyKey: メソッドの名前。
- 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. プロパティデコレータ
プロパティデコレータはプロパティ宣言に適用されます。パラメータとして target と propertyKey を受け取ります。
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. 評価の順序
複数のデコレータが適用されている場合、以下の順序で評価・適用されます。
- インスタンスメンバーに対して、パラメータデコレータ、続いてメソッド/プロパティデコレータ。
- 静的メンバーに対して、パラメータデコレータ、続いてメソッド/プロパティデコレータ。
- コンストラクタに対して、パラメータデコレータ。
- クラスに対して、クラスデコレータ。
デコレータ関数自体の実行は「下から上(内側から外側)」に向かって行われます。
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/:id10. ベストプラクティス
- 責務を明確にする: 一つのデコレータには一つの役割だけを持たせます。
- ドキュメント化: デコレータがどのような副作用を持つかを明示します。
- ファクトリの活用: 設定可能なデコレータを作成し、汎用性を高めます。
- パフォーマンスへの配慮: ランタイムのオーバーヘッドを意識し、クリティカルな箇所での過度な使用は避けます。
- reflect-metadata の利用: 高度な実行時型情報が必要な場合に検討します。
11. よくある落とし穴
- 有効化の忘れ:
tsconfig.jsonのexperimentalDecoratorsを確認してください。 - シグネチャの誤り: 各デコレータタイプには固有の引数があります。
- 評価順序の誤解: デコレータは記述順の下から上に適用される点に注意してください。
- プロパティの初期化: プロパティデコレータは、インスタンスのプロパティが初期化される前に実行されます。
- ブラウザ互換性: 古いブラウザではトランスパイルが必要になる場合があります。