TypeScriptのデコレータ
デコレータは実験的な構文であり、@decorator構文を用いて、クラス、メソッド、プロパティにメタデータを追加したり、それらの挙動を変更したりします。NestJSやAngularなどのフレームワークで広く利用されています。
1. デコレータの概要
(1) デコレーターとは何か?
デコレータとは関数のことであり、引数として対象(クラス、メソッド、またはプロパティ)を受け取り、その対象の挙動を変更したり強化したりすることができます:
TYPESCRIPT
// Decorator Functions
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Call ${key},Parameters:${args}`);
return original.apply(this, args);
};
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
let calc = new Calculator();
calc.add(1, 2);
// Output:Call add,Parameters:1,2
(2) デコレータのサポートを有効にする
デコレータは実験的な機能であり、tsconfig.jsonで有効にする必要があります:
JSON
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
(3) 5種類のデコレーター
| タイプ | 変更後のターゲット | パラメータ |
|---|---|---|
| クラスデコレータ | クラス定義 | コンストラクタ |
| メソッドデコレータ | クラスメソッド | ターゲット、メソッド名、記述子 |
| プロパティ・デコレータ | クラスのプロパティ | ターゲット、プロパティ名 |
| パラメータデコレータ | 関数のパラメータ | ターゲット、メソッド名、パラメータのインデックス |
| アクセサ・デコレータ | ゲッター/セッター | ターゲット、アクセサ名、記述子 |
2. クラス・デコレータ
クラスデコレータはコンストラクタを引数として受け取り、クラスの定義を変更または置き換えることができます:
(1) 基本的な使い方
TYPESCRIPT
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Config {
host: string = "localhost";
port: number = 3000;
}
// Config Has been sealed——New properties cannot be added
(2) デコレータファクトリ
パラメータが必要な場合は、ファクトリ関数を使用してデコレータを返します:
TYPESCRIPT
function className(prefix: string) {
return function (constructor: Function) {
constructor.prototype._displayName = `${prefix}_${constructor.name}`;
};
}
@className("App")
class UserService {}
// UserService.prototype._displayName = "App_UserService"
(3) コンストラクタのオーバーライド
TYPESCRIPT
function logged<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
constructor(...args: any[]) {
console.log(`Create ${constructor.name},Parameters:${args}`);
super(...args);
}
};
}
@logged
class User {
constructor(public name: string, public age: number) {}
}
let user = new User("Charlie", 20);
// Output:Create User,Parameters:Charlie,20
▶ 例:登録デコレータ—クラスの自動収集
TYPESCRIPT
const registry: Map<string, any> = new Map();
function Register(name: string) {
return function <T extends { new (...args: any[]): {} }>(constructor: T) {
registry.set(name, constructor);
return constructor;
};
}
@register("user")
class UserService {
getUser(id: number) { return { id, name: "User" + id }; }
}
@register("product")
class ProductService {
getProduct(id: number) { return { id, title: "Products" + id }; }
}
// Get a Service by Name
function getService(name: string) {
let Service = registry.get(name);
if (!Service) throw new Error(`Unregistered Services:${name}`);
return new Service();
}
let user = (getService("user") as UserService).getUser(1);
let product = (getService("product") as ProductService).getProduct(2);
console.log(user.name); // "User1"
console.log(product.title); // "Products2"
3. メソッドデコレータ
メソッドデコレータは、メソッドの定義を監視、変更、または置換することができます:
(1) 基本的な使い方
TYPESCRIPT
function enumerable(value: boolean) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class Person {
constructor(public name: string) {}
@enumerable(false)
getFullName(): string {
return this.name;
}
}
(2) メソッド実行ログ
TYPESCRIPT
function measure(target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const start = performance.now();
const result = original.apply(this, args);
const end = performance.now();
console.log(`${key} Execution Time:${(end - start).toFixed(2)}ms`);
return result;
};
return descriptor;
}
class DataProcessor {
@measure
processLargeArray(data: number[]): number {
return data.reduce((sum, n) => sum + n, 0);
}
}
let processor = new DataProcessor();
processor.processLargeArray(Array.from({ length: 1000000 }, (_, i) => i));
// Output:processLargeArray Execution Time:XX.XXms
(3) ジッター対策手法
TYPESCRIPT
function debounce(delay: number) {
return function (target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
let timer: any;
descriptor.value = function (...args: any[]) {
clearTimeout(timer);
timer = setTimeout(() => original.apply(this, args), delay);
};
return descriptor;
};
}
class SearchBox {
@debounce(300)
search(query: string): void {
console.log(`Search:${query}`);
}
}
let box = new SearchBox();
box.search("T"); // 300ms Then, if there is no new input, execute it
box.search("Ty");
box.search("Typ");
box.search("TypeScript"); // This will be executed only once
4. プロパティ・デコレータとパラメータ・デコレータ
(1) プロパティ・デコレータ
TYPESCRIPT
function format(formatStr: string) {
return function (target: any, key: string) {
// Store formatting information in metadata
Reflect.defineMetadata("format", formatStr, target, key);
};
}
class User {
@format("YYYY-MM-DD")
birthday: string = "2000-01-15";
@format("HH:mm:ss")
loginTime: string = "09:30:00";
}
(2) パラメータデコレータ
TYPESCRIPT
function required(target: any, key: string, index: number) {
const existing: number[] = Reflect.getMetadata("required", target, key) || [];
existing.push(index);
Reflect.defineMetadata("required", existing, target, key);
}
class UserService {
createUser(@required name: string, @required email: string, age?: number) {
// Parameter validation is handled by the framework at runtime.
}
}
5. デコレータの組み合わせ
複数のデコレータを同時に使用することができます。これらは下から上へと順に実行されます(対象に最も近いものが最初に実行されます):
TYPESCRIPT
function log1(target: any, key: string, descriptor: PropertyDescriptor) {
console.log("log1 Applications");
}
function log2(target: any, key: string, descriptor: PropertyDescriptor) {
console.log("log2 Applications");
}
class Example {
@log1 // The Second Application(Outer layer)
@log2 // The First Application(Inner layer,Approach Methods)
method() {}
}
// Output:log2 Applications → log1 Applications
(1) 一般的な組み合わせパターン
TYPESCRIPT
class ApiController {
@logged
@measure
@debounce(100)
async fetchData(url: string): Promise<any> {
// First debounce → then measure → then logged(Bottom-up application)
}
}
❓ よくある質問
Q デコレータは安定した機能ですか?
A 現在は実験的な機能(Stage 3 提案)です。TypeScript 5.0 以降では、新しい Stage 3 デコレータ構文がサポートされています(
experimentalDecorators は不要になりました)が、旧構文も引き続き使用可能です。NestJS や Angular などのフレームワークでは、現在も旧構文が使用されています。ご利用のフレームワークが提供するガイドラインに従うことをお勧めします。Q デコレータは関数と一緒に使用できますか?
A いいえ。デコレータはクラスおよびクラスのメンバー(メソッド、プロパティ、パラメータ)でのみ使用できます。通常の関数はデコレータをサポートしていません。これはJavaScript仕様案の設計上の制限です。関数を拡張する必要がある場合は、高階関数(ラッパー)パターンを使用してください。
Q デコレータはパフォーマンスにどのような影響を与えますか?
A デコレータはクラスが定義された際に一度だけ実行される(呼び出しのたびに実行されるわけではない)ため、オーバーヘッドはごくわずかです。ラップされた関数を変更するメソッドデコレータの場合、呼び出しのたびにわずかなオーバーヘッド(追加の関数呼び出し)が発生しますが、通常は無視できる程度です。パフォーマンスが重要なシナリオでは、ホットパスに対して複数のデコレータを連鎖して使用することは避けてください。
Q デコレータを使わずに同じ効果を得ることはできますか?
A はい。デコレータは本質的に構文上の糖衣であり、
@log method()はmethod = log(method)と同等です。デコレータを使わなくても、高階関数、ミックスインパターン、AOPライブラリ、その他の手法を用いて同じ結果を得ることができます。デコレータの利点は、宣言的であり、直感的で、よりクリーンなコードが得られることです。📖 まとめ
- デコレータは、
@decoratorという構文を使用して、クラスやクラスのメンバーにメタデータを追加したり、それらの動作を変更したりします - 5種類のデコレータ:クラス、メソッド、プロパティ、パラメータ、アクセサ。それぞれが異なるパラメータのシグネチャを持つ
- デコレータファクトリはデコレータ関数を返す。これにより、デコレータはパラメータを受け取ることができるようになる
- クラスデコレータはコンストラクタの代わりとなり(新しいクラスを返す)、メソッドデコレータはメソッドをラップすることができます
- 複数のデコレータは下から上へと適用されます(対象に最も近いものが最初に実行されます)
- デコレータは、
experimentalDecoratorsオプションを必要とする実験的な機能です
📝 練習問題
- 基本演習(難易度 ⭐):メソッドをオーバーライド不可にする
@readonlyメソッドデコレータを作成してください(writable: false)。これをクラスのメソッドに適用し、サブクラスでそのメソッドをオーバーライドしてみて、その結果を確認してください。 - 上級問題(難易度 ⭐⭐):デコレータされたメソッドが呼び出された際に非推奨の警告を出力する
@deprecated(message)デコレータファクトリを作成してください。ヒント:メソッドラッパー関数にconsole.warnを追加してください。 - 課題(難易度:⭐⭐⭐):クラスデコレータを使用して、簡単な依存性注入を実装してください。サービスクラスに
@Injectable()を、依存プロパティに@Inject(Service)を指定すると、Container.resolve(TargetClass)が自動的に依存関係を解決して注入します。



