TypeScriptのデコレータ(Decorators)は、クラス・メソッド・プロパティ・パラメータに宣言的な注釈を付けて機能を追加できる強力な機能です。Angular・NestJSでは不可欠な構文であり、ログ・バリデーション・依存性注入(DI)などのパターンをエレガントに実装できます。
本記事では、4種類のデコレータの書き方から実行順序・ファクトリ・実務活用まで、実例コードとともに徹底解説します。
デコレータとは
デコレータは @expression の形式で記述し、クラスや그 메서드에 기능을 추가하는仕組みです。デコレータはメタプログラミングの一種で、コードの振る舞いを宣言的に変更できます。
① クラスデコレータ ② メソッドデコレータ ③ アクセサデコレータ ④ プロパティデコレータ ⑤ パラメータデコレータ
@sealed // クラスデコレータ
class BugReport {
@format("Hello") // プロパティデコレータ
title: string;
@configurable(false) // メソッドデコレータ
greet(@required msg: string) { // パラメータデコレータ
return msg;
}
}
デコレータは 実行時(ランタイム) に評価され、クラス定義が読み込まれた時点で適用されます。
tsconfig設定
従来のデコレータを使うには tsconfig.json で以下を有効化します。(TypeScript 5.0以降の新デコレータ仕様では不要な設定もあります)
{
"compilerOptions": {
"target": "ES2017",
"experimentalDecorators": true, // デコレータ有効化
"emitDecoratorMetadata": true // メタデータ出力(DI等に必要)
}
}
名前の通り「実験的」機能です。TypeScript 5.0でStage 3の新デコレータ仕様が追加されましたが、NestJS・Angular等の既存フレームワークは引き続き
experimentalDecorators: true を使います。詳細は TypeScript 5.0の新デコレータ をご参照ください。また、emitDecoratorMetadata を使う場合は reflect-metadata パッケージのインストールが必要です。
npm install reflect-metadata # エントリポイントでimport import "reflect-metadata";
tsconfig全体の設定については tsconfig.json 完全ガイド も参照してください。
クラスデコレータ
クラスデコレータはクラス宣言の直前に置かれ、クラスのコンストラクタを引数として受け取ります。
// クラスデコレータの型
type ClassDecorator = (target: Function) => void | Function;
// シンプルなクラスデコレータ
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
console.log(`${constructor.name} クラスをシールしました`);
}
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
const b = new BugReport("TypeScriptエラー");
// BugReport クラスをシールしました → コンストラクタへの追加不可
コンストラクタを差し替えてクラスを拡張することもできます:
function withTimestamp<T extends { new (...args: any[]): {} }>(Base: T) {
return class extends Base {
createdAt = new Date().toISOString();
};
}
@withTimestamp
class User {
constructor(public name: string) {}
}
const u = new User("Alice") as User & { createdAt: string };
console.log(u.createdAt); // "2024-01-15T10:30:00.000Z"
メソッドデコレータ
メソッドデコレータは3つの引数を受け取ります:
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`[${propertyKey}] 引数:`, args);
const result = original.apply(this, args);
console.log(`[${propertyKey}] 戻り値:`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
// [add] 引数: [2, 3]
// [add] 戻り値: 5
元のメソッドを保存してから新しい関数で包む(ラッパーパターン)。
apply(this, args) で元の this コンテキストを維持することが重要です。プロパティデコレータ
プロパティデコレータはクラスのプロパティ宣言の直前に記述します。引数は target(プロトタイプ)と propertyKey(プロパティ名)の2つです。
function readonly(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
writable: false,
configurable: false,
});
}
class Config {
@readonly
version = "1.0.0";
}
const config = new Config();
// config.version = "2.0.0"; // エラー: 書き込み不可
バリデーションの実装例:
import "reflect-metadata";
function minLength(min: number) {
return function (target: any, propertyKey: string) {
Reflect.defineMetadata("minLength", min, target, propertyKey);
};
}
function validate(obj: any): boolean {
for (const key of Object.keys(obj)) {
const min = Reflect.getMetadata("minLength", obj, key);
if (min !== undefined && String(obj[key]).length < min) {
console.error(`${key} は ${min}文字以上必要です`);
return false;
}
}
return true;
}
class User {
@minLength(3)
name: string;
constructor(name: string) {
this.name = name;
}
}
const u = new User("AB");
validate(u); // name は 3文字以上必要です
パラメータデコレータ
パラメータデコレータはメソッドのパラメータ宣言の直前に置き、3つの引数を受け取ります。
function required(target: any, propertyKey: string, parameterIndex: number) {
const existingRequired: number[] =
Reflect.getMetadata("required", target, propertyKey) || [];
existingRequired.push(parameterIndex);
Reflect.defineMetadata("required", existingRequired, target, propertyKey);
}
function validate(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const method = descriptor.value;
descriptor.value = function (...args: any[]) {
const required: number[] =
Reflect.getMetadata("required", target, propertyKey) || [];
for (const idx of required) {
if (args[idx] === undefined) {
throw new Error(`引数 ${idx} は必須です`);
}
}
return method.apply(this, args);
};
}
class Greeter {
@validate
greet(@required name: string, greeting?: string) {
return `${greeting ?? "Hello"}, ${name}!`;
}
}
const g = new Greeter();
g.greet("Alice"); // "Hello, Alice!"
g.greet(undefined as any); // Error: 引数 0 は必須です
デコレータファクトリ
デコレータファクトリは引数を受け取ってデコレータ関数を返す関数です。カスタマイズ可能なデコレータを作成する際に使います。
// デコレータファクトリ: 引数 → デコレータ関数
function retry(maxAttempts: number, delay: number = 1000) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await original.apply(this, args);
} catch (err) {
console.warn(`試行 ${attempt}/${maxAttempts} 失敗: ${err}`);
if (attempt === maxAttempts) throw err;
await new Promise(r => setTimeout(r, delay));
}
}
};
return descriptor;
};
}
class ApiClient {
@retry(3, 500) // 最大3回、500ms間隔でリトライ
async fetchUser(id: number) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}
}
引数なしデコレータ(
@log)は再利用できるが柔軟性に欠ける。ファクトリ(@retry(3, 500))にすれば設定値をその場で渡せて汎用的に使えます。実行順序
複数のデコレータを適用する場合、書いた順(上から下)で評価され、適用は下から上(内側から外側)の順で行われます。
function first() {
console.log("first(): evaluated");
return function (target: any, key: string, desc: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): evaluated");
return function (target: any, key: string, desc: PropertyDescriptor) {
console.log("second(): called");
};
}
class Example {
@first()
@second()
method() {}
}
// 出力:
// first(): evaluated ← 上から評価
// second(): evaluated ← 上から評価
// second(): called ← 下から適用
// first(): called ← 下から適用
実務活用パターン
① パフォーマンス計測
function measure(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = async function (...args: any[]) {
const start = performance.now();
const result = await original.apply(this, args);
const ms = (performance.now() - start).toFixed(2);
console.log(`[${propertyKey}] ${ms}ms`);
return result;
};
}
class UserService {
@measure
async getUsers(): Promise<User[]> {
return await db.query("SELECT * FROM users");
}
}
// [getUsers] 42.31ms
② キャッシュ(メモ化)
function memoize(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const cache = new Map<string, any>();
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log(`キャッシュヒット: ${propertyKey}(${key})`);
return cache.get(key);
}
const result = original.apply(this, args);
cache.set(key, result);
return result;
};
}
class MathService {
@memoize
fibonacci(n: number): number {
if (n <= 1) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
③ NestJS スタイルの DI
NestJSは独自のDIコンテナを持ちますが、デコレータを使った簡易DIコンテナのイメージを示します:
const container = new Map<string, any>();
function Injectable() {
return function (constructor: Function) {
container.set(constructor.name, new (constructor as any)());
};
}
function Inject(token: string) {
return function (target: any, key: string) {
Object.defineProperty(target, key, {
get: () => container.get(token),
});
};
}
@Injectable()
class LogService {
log(msg: string) { console.log(`[LOG] ${msg}`); }
}
class UserController {
@Inject("LogService")
private logger!: LogService;
createUser(name: string) {
this.logger.log(`ユーザー作成: ${name}`);
}
}
const ctrl = new UserController();
ctrl.createUser("Alice"); // [LOG] ユーザー作成: Alice
TypeScript 5.0の新デコレータ(Stage 3仕様)
TypeScript 5.0(2023年3月)でECMAScript Stage 3デコレータが正式サポートされました。experimentalDecorators: true が不要で、型安全性も向上しています。
// TypeScript 5.0 Stage 3 デコレータの書き方
function logged<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacement(this: This, ...args: Args): Return {
console.log(`${methodName} を呼び出し`);
const result = target.call(this, ...args);
console.log(`${methodName} が完了`);
return result;
}
return replacement;
}
class Person {
name: string;
constructor(name: string) { this.name = name; }
@logged // experimentalDecorators不要
greet() {
console.log(`Hello, I am ${this.name}`);
}
}
const p = new Person("Alice");
p.greet();
// greet を呼び出し
// Hello, I am Alice
// greet が完了
NestJS・Angularプロジェクトは当面
experimentalDecorators: true を継続。新規の素のTypeScriptプロジェクトならStage 3デコレータを検討する価値あり。両者は 共存できない(同一ファイル内での混在不可)ため注意。よくあるエラーと対処法
// BAD: 変数にデコレータは不可
@log
const x = 42; // エラー: Decorators are not valid here
// GOOD: クラスのメソッドに適用
class Foo {
@log
bar() {}
}
まとめ
デコレータを理解することで、横断的関心事(ロギング・認証・キャッシュ)をビジネスロジックから分離でき、コードがシンプルで再利用性の高い構造になります。
関連記事:
FAQ
Qデコレータはランタイムに実行されますか?
Aはい。デコレータはクラス定義が評価されるタイミング(モジュール読み込み時)に実行されます。インスタンス生成のたびには実行されません。
Qデコレータとミックスイン(Mixin)の違いは?
Aミックスインはクラス継承で機能を合成する手法。デコレータは既存クラスに後付けで機能を付与します。用途に応じて使い分けますが、デコレータのほうが宣言的で読みやすいコードになります。
QReact でもデコレータを使えますか?
AReactそのものはデコレータを必須としませんが、MobXの @observable や一部状態管理ライブラリではデコレータを活用しています。experimentalDecorators: true を有効にすれば利用可能です。
Qメソッドデコレータの descriptor.value と descriptor.get の違いは?
Adescriptor.value は通常のメソッド用。アクセサ(get/set)には descriptor.get/descriptor.set を使います。両者を混同するとエラーになるため注意。
QTypeScript 5.0の新デコレータに移行すべきですか?
ANestJS・Angularなどの主要フレームワークは現在も experimentalDecorators ベースです。新規の小規模プロジェクトではStage 3の採用を検討できますが、フレームワークを使う場合は当面は従来仕様に従ってください。