TypeScript 5.8デコレーターミドルウェア:メタプログラミングからAOPアーキテクチャまで6つのプロダクションパターン
TypeScriptデコレーターがついにプロダクション対応に
まだexperimentalDecoratorsとemitDecoratorMetadataを使っている?TypeScriptのアップグレードのたびに破壊的変更を心配している?2026年、TC39デコレーター仕様はStage 3.7に到達し、TypeScript 5.8は標準デコレーターをネイティブサポート——実験的フラグは不要、意味的曖昧さもなく、デコレーターミドルウェアはついにプロダクションで安心して使える。メソッドインターセプターからAOPアーキテクチャ、パラメーターバリデーションから依存性注入まで、デコレーターはTypeScriptのメタプログラミングパラダイムを再定義している。
本記事ではTC39デコレーターのコア概念から出発し、メソッドインターセプター&ロギングデコレーター→クラストランスフォーマー&メタデータ登録→パラメーターバリデーションデコレーター→AOPミドルウェアチェーン→依存性注入コンテナ→デコレーター合成&パイプラインパターンの6つのプロダクションパターンを、仕様から実装まで一歩ずつ解説する。
主要ポイント
- TC39標準デコレーターとLegacyデコレーターの本質的な違い:被装飾オブジェクトを変更せず、置換/ラップする
- デコレーターミドルウェアの3層アーキテクチャ:定義層(Decorator)→ 登録層(Metadata)→ 実行層(Middleware Chain)
- 6つのプロダクションパターンでロギング、バリデーション、AOP、DI、パイプライン合成をカバー
- 5つのよくある落とし穴と解決策、10のよくあるエラートラブルシューティング
- 完全比較:TC39デコレーター vs Legacyデコレーター vs デコレーターファクトリ
目次
- TC39デコレーターコア概念
- パターン1:メソッドインターセプター&ロギングデコレーター
- パターン2:クラストランスフォーマー&メタデータ登録
- パターン3:パラメーターバリデーションデコレーター
- パターン4:AOPミドルウェアチェーン
- パターン5:依存性注入コンテナ
- パターン6:デコレーター合成&パイプラインパターン
- 5つのよくある落とし穴と解決策
- 10のよくあるエラートラブルシューティング
- 高度な最適化テクニック
- 比較分析:TC39デコレーター vs Legacyデコレーター vs デコレーターファクトリ
- オンラインツール推奨
- まとめ
TC39デコレーターコア概念
| 概念 | 説明 |
|---|---|
| TC39 Decorator | ECMAScript標準デコレーター提案、Stage 3.7、TypeScript 5.8ネイティブサポート |
| Method Decorator | メソッドデコレーター、メソッド呼び出しをインターセプト、引数/戻り値/例外を変更可能 |
| Class Decorator | クラスデコレーター、クラス定義を置換または拡張、メタデータ注入可能 |
| Accessor Decorator | getter/setterデコレーター、プロパティアクセスをインターセプト |
| Field Decorator | フィールドデコレーター、初期化時にトリガー、初期値の置換可能 |
| Decorator Context | デコレーターコンテキストオブジェクト、kind、name、access等のメタ情報を提供 |
| Add Initializer | クラスインスタンス化時に自動実行される初期化フック |
| Metadata | Symbolを通じてクラス/メソッドに付加されるメタデータ、DIやバリデーションに使用 |
TC39デコレーター実行フロー
定義フェーズ:
@log デコレーターファクトリ呼び出し
@validate デコレーター関数を返す
class UserService { デコレーターは下から上の順序で実行
@cache メソッドデコレーターが先に実行
async getUser(id: string) { クラスデコレーターが最後に実行
...
}
}
実行フェーズ:
const instance = new UserService() → addInitializerフックをトリガー
instance.getUser('123') → メソッドデコレーターのラップロジックをトリガー
デコレーターコンテキスト(DecoratorContext):
{
kind: 'method' | 'class' | 'accessor' | 'field',
name: 'getUser' | 'UserService',
access: { get, set, has },
addInitializer(fn) → 初期化フックを登録,
static: boolean,
private: boolean
}
TC39 vs Legacyデコレーターコアの違い
Legacyデコレーター(experimentalDecorators):
@log 元のディスクリプタを変更
class Svc { 順序:上から下
descriptor.value = wrappedFn 被装飾オブジェクトを直接変更
}
TC39標準デコレーター:
@log 置換値を返す
class Svc { 順序:下から上
return { ...descriptor, 元のオブジェクトを変更せず、新しいものを返す
method: wrappedFn } 関数型、不変
}
パターン1:メソッドインターセプター&ロギングデコレーター
メソッドインターセプターはデコレーターミドルウェアの最も基本的なパターン——元のメソッドコードを変更せずに、メソッド呼び出しをインターセプトし、ロギング、タイミング、リトライ等の横断的関心事を追加する。
基本ロギングデコレーター
type DecoratorTarget = {
[key: string]: (...args: any[]) => any;
};
function log(
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: DecoratorTarget, ...args: any[]) {
const startTime = performance.now();
const className = this.constructor.name;
console.log(`[${className}.${methodName}] 呼び出し開始`, {
args: args.length > 0 ? args : undefined,
timestamp: new Date().toISOString(),
});
try {
const result = target.value.apply(this, args);
if (result instanceof Promise) {
return result.then(
(resolvedValue) => {
const duration = performance.now() - startTime;
console.log(`[${className}.${methodName}] 呼び出し成功`, {
duration: `${duration.toFixed(2)}ms`,
hasResult: resolvedValue !== undefined,
});
return resolvedValue;
},
(error) => {
const duration = performance.now() - startTime;
console.error(`[${className}.${methodName}] 非同期呼び出し失敗`, {
duration: `${duration.toFixed(2)}ms`,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
);
}
const duration = performance.now() - startTime;
console.log(`[${className}.${methodName}] 呼び出し成功`, {
duration: `${duration.toFixed(2)}ms`,
});
return result;
} catch (error) {
const duration = performance.now() - startTime;
console.error(`[${className}.${methodName}] 同期呼び出し失敗`, {
duration: `${duration.toFixed(2)}ms`,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
return replacementMethod;
}
設定可能なロギングデコレーターファクトリ
interface LogOptions {
level?: 'debug' | 'info' | 'warn' | 'error';
includeArgs?: boolean;
includeResult?: boolean;
maxArgLength?: number;
slowThreshold?: number;
}
function createLogDecorator(options: LogOptions = {}) {
const {
level = 'info',
includeArgs = true,
includeResult = false,
maxArgLength = 200,
slowThreshold = 1000,
} = options;
return function logDecorator(
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: DecoratorTarget, ...args: any[]) {
const startTime = performance.now();
const className = this.constructor.name;
const logData: Record<string, unknown> = {
method: `${className}.${methodName}`,
timestamp: new Date().toISOString(),
};
if (includeArgs && args.length > 0) {
const serializedArgs = JSON.stringify(args);
logData.args = serializedArgs.length > maxArgLength
? serializedArgs.slice(0, maxArgLength) + '...'
: args;
}
const logFn = console[level] || console.info;
logFn(`[LOG] ${logData.method} 呼び出し`, logData);
try {
const result = target.value.apply(this, args);
if (result instanceof Promise) {
return result.then(
(resolvedValue) => {
const duration = performance.now() - startTime;
const resultLogData: Record<string, unknown> = {
method: `${className}.${methodName}`,
duration: `${duration.toFixed(2)}ms`,
};
if (includeResult) {
resultLogData.result = resolvedValue;
}
if (duration > slowThreshold) {
console.warn(
`[SLOW] ${className}.${methodName} 所要時間 ${duration.toFixed(2)}ms`,
resultLogData
);
} else {
logFn(`[LOG] ${className}.${methodName} 完了`, resultLogData);
}
return resolvedValue;
},
(error) => {
const duration = performance.now() - startTime;
console.error(`[ERROR] ${className}.${methodName} 非同期失敗`, {
duration: `${duration.toFixed(2)}ms`,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
);
}
const duration = performance.now() - startTime;
const resultLogData: Record<string, unknown> = {
method: `${className}.${methodName}`,
duration: `${duration.toFixed(2)}ms`,
};
if (includeResult) {
resultLogData.result = result;
}
logFn(`[LOG] ${className}.${methodName} 完了`, resultLogData);
return result;
} catch (error) {
const duration = performance.now() - startTime;
console.error(`[ERROR] ${className}.${methodName} 同期失敗`, {
duration: `${duration.toFixed(2)}ms`,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
return replacementMethod;
};
}
const logVerbose = createLogDecorator({
level: 'debug',
includeArgs: true,
includeResult: true,
slowThreshold: 500,
});
const logMinimal = createLogDecorator({
level: 'info',
includeArgs: false,
includeResult: false,
slowThreshold: 2000,
});
使用例
class PaymentService {
@logVerbose
async processPayment(orderId: string, amount: number): Promise<string> {
await new Promise((resolve) => setTimeout(resolve, 100));
if (amount <= 0) {
throw new Error('金額は0より大きい必要があります');
}
return `PAY-${orderId}-${Date.now()}`;
}
@logMinimal
getPaymentStatus(paymentId: string): string {
return 'completed';
}
}
const paymentService = new PaymentService();
await paymentService.processPayment('ORD-001', 99.99);
パターン2:クラストランスフォーマー&メタデータ登録
クラスデコレーターはクラス定義全体を置換したり、addInitializerを通じてインスタンス化時にメタデータを注入できる。これは依存性注入、ルート登録、ORMマッピングの基盤となる。
クラスデコレーターの基本:ルートメタデータ登録
const ROUTE_METADATA_KEY = Symbol('route-metadata');
const CONTROLLER_METADATA_KEY = Symbol('controller-metadata');
interface RouteMetadata {
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
path: string;
handlerName: string;
}
interface ControllerMetadata {
basePath: string;
routes: RouteMetadata[];
}
function Controller(basePath: string) {
return function <T extends { new (...args: any[]): any }>(
target: T,
context: ClassDecoratorContext
) {
const routes: RouteMetadata[] = Reflect.getMetadata(
ROUTE_METADATA_KEY,
target.prototype
) || [];
const metadata: ControllerMetadata = {
basePath,
routes,
};
Reflect.defineMetadata(
CONTROLLER_METADATA_KEY,
metadata,
target
);
return class extends target {
constructor(...args: any[]) {
super(...args);
console.log(`[Controller] ${basePath} 登録完了、${routes.length}ルート含む`);
}
};
};
}
function Route(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string) {
return function (
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const handlerName = String(context.name);
const existingRoutes: RouteMetadata[] = Reflect.getMetadata(
ROUTE_METADATA_KEY,
context.static ? target : target
) || [];
existingRoutes.push({ method, path, handlerName });
Reflect.defineMetadata(
ROUTE_METADATA_KEY,
existingRoutes,
context.static ? target : target
);
return target.value;
};
}
メタデータ登録とクエリ
const VALIDATION_METADATA_KEY = Symbol('validation-metadata');
const FIELD_METADATA_KEY = Symbol('field-metadata');
interface FieldMetadata {
fieldName: string;
type: string;
required: boolean;
validator?: (value: any) => boolean;
errorMessage?: string;
}
interface ValidationMetadata {
fields: FieldMetadata[];
schemaName: string;
}
function Field(options: Omit<FieldMetadata, 'fieldName'> = {}) {
return function (
target: ClassAccessorDecoratorTarget<any, any>,
context: ClassAccessorDecoratorContext
) {
const fieldName = String(context.name);
const fieldMeta: FieldMetadata = {
fieldName,
type: options.type || 'string',
required: options.required ?? true,
validator: options.validator,
errorMessage: options.errorMessage,
};
context.addInitializer(function (this: any) {
const existingFields: FieldMetadata[] = Reflect.getMetadata(
FIELD_METADATA_KEY,
this.constructor
) || [];
existingFields.push(fieldMeta);
Reflect.defineMetadata(FIELD_METADATA_KEY, existingFields, this.constructor);
});
return {
get(this: any) {
return target.get.call(this);
},
set(this: any, value: any) {
if (fieldMeta.required && (value === null || value === undefined)) {
throw new Error(`${fieldName} は必須フィールドです`);
}
if (fieldMeta.validator && !fieldMeta.validator(value)) {
throw new Error(fieldMeta.errorMessage || `${fieldName} バリデーション失敗`);
}
target.set.call(this, value);
},
};
};
}
function Validatable(schemaName: string) {
return function <T extends { new (...args: any[]): any }>(
target: T,
context: ClassDecoratorContext
) {
return class extends target {
constructor(...args: any[]) {
super(...args);
const fields: FieldMetadata[] = Reflect.getMetadata(
FIELD_METADATA_KEY,
this.constructor
) || [];
const metadata: ValidationMetadata = { fields, schemaName };
Reflect.defineMetadata(VALIDATION_METADATA_KEY, metadata, this.constructor);
}
static getValidationSchema(): ValidationMetadata {
const fields: FieldMetadata[] = Reflect.getMetadata(
FIELD_METADATA_KEY,
this
) || [];
return { fields, schemaName };
}
validate(): { valid: boolean; errors: string[] } {
const errors: string[] = [];
const fields: FieldMetadata[] = Reflect.getMetadata(
FIELD_METADATA_KEY,
this.constructor
) || [];
for (const field of fields) {
const value = (this as any)[field.fieldName];
if (field.required && (value === null || value === undefined)) {
errors.push(`${field.fieldName} は必須フィールドです`);
}
if (field.validator && !field.validator(value)) {
errors.push(field.errorMessage || `${field.fieldName} バリデーション失敗`);
}
}
return { valid: errors.length === 0, errors };
}
};
};
}
使用例
@Validatable('User')
class User {
@Field({ type: 'string', required: true })
accessor name: string = '';
@Field({
type: 'string',
required: true,
validator: (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
errorMessage: 'メールアドレスの形式が正しくありません',
})
accessor email: string = '';
@Field({ type: 'number', required: false })
accessor age: number = 0;
}
const user = new User();
user.name = '田中';
user.email = 'tanaka@example.com';
user.age = 28;
const result = user.validate();
console.log(result);
const schema = User.getValidationSchema();
console.log(schema);
パターン3:パラメーターバリデーションデコレーター
パラメーターバリデーションはデコレーターミドルウェアの最も実用的なシナリオの一つ——メソッド実行前にパラメーターの型と制約を自動的に検証し、ランタイム型エラーを防止する。
型バリデーションデコレーター
interface ValidationRule {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
required?: boolean;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
pattern?: RegExp;
custom?: (value: any) => boolean;
message?: string;
}
function validateParams(...rules: ValidationRule[]) {
return function (
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: DecoratorTarget, ...args: any[]) {
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
const value = args[i];
const paramName = `パラメータ${i + 1}`;
if (rule.required && (value === null || value === undefined)) {
throw new TypeError(
`[${this.constructor.name}.${methodName}] ${paramName} は必須です`
);
}
if (value !== null && value !== undefined) {
const actualType = Array.isArray(value) ? 'array' : typeof value;
if (actualType !== rule.type) {
throw new TypeError(
`[${this.constructor.name}.${methodName}] ${paramName} 型エラー:期待 ${rule.type}、実際 ${actualType}`
);
}
if (rule.type === 'string') {
if (rule.minLength && value.length < rule.minLength) {
throw new RangeError(
`[${this.constructor.name}.${methodName}] ${paramName} 最小長 ${rule.minLength}`
);
}
if (rule.maxLength && value.length > rule.maxLength) {
throw new RangeError(
`[${this.constructor.name}.${methodName}] ${paramName} 最大長 ${rule.maxLength}`
);
}
if (rule.pattern && !rule.pattern.test(value)) {
throw new Error(
rule.message || `[${this.constructor.name}.${methodName}] ${paramName} 形式が正しくありません`
);
}
}
if (rule.type === 'number') {
if (rule.min !== undefined && value < rule.min) {
throw new RangeError(
`[${this.constructor.name}.${methodName}] ${paramName} 最小値 ${rule.min}`
);
}
if (rule.max !== undefined && value > rule.max) {
throw new RangeError(
`[${this.constructor.name}.${methodName}] ${paramName} 最大値 ${rule.max}`
);
}
}
if (rule.custom && !rule.custom(value)) {
throw new Error(
rule.message || `[${this.constructor.name}.${methodName}] ${paramName} バリデーション失敗`
);
}
}
}
return target.value.apply(this, args);
}
return replacementMethod;
};
}
スキーマバリデーションデコレーター(Zod統合)
import { z, ZodSchema } from 'zod';
function validateWithSchema(schemas: ZodSchema[]) {
return function (
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
function replacementMethod(this: DecoratorTarget, ...args: any[]) {
for (let i = 0; i < schemas.length; i++) {
if (i >= args.length) continue;
const result = schemas[i].safeParse(args[i]);
if (!result.success) {
const errors = result.error.issues.map(
(issue) => `${issue.path.join('.')}: ${issue.message}`
);
throw new Error(
`[${this.constructor.name}.${methodName}] パラメータ${i + 1}バリデーション失敗: ${errors.join('; ')}`
);
}
args[i] = result.data;
}
return target.value.apply(this, args);
}
return replacementMethod;
};
}
使用例
const OrderIdSchema = z.string().uuid();
const CreateOrderSchema = z.object({
productId: z.string().min(1),
quantity: z.number().int().min(1).max(999),
price: z.number().positive(),
address: z.object({
city: z.string().min(1),
street: z.string().min(1),
zipCode: z.string().regex(/^\d{6}$/),
}),
});
class OrderService {
@validateParams(
{ type: 'string', required: true, pattern: /^[A-Z]{2}-\d{4}$/, message: '注文番号形式:XX-0000' },
{ type: 'number', required: true, min: 0, max: 1000000 }
)
createOrder(orderId: string, amount: number): string {
return `注文 ${orderId} 作成完了、金額 ${amount}`;
}
@validateWithSchema([OrderIdSchema, CreateOrderSchema])
async processOrder(orderId: string, orderData: z.infer<typeof CreateOrderSchema>) {
return { orderId, status: 'processed', ...orderData };
}
}
const orderService = new OrderService();
orderService.createOrder('CN-1234', 99.99);
try {
orderService.createOrder('invalid', -1);
} catch (error) {
console.error(error);
}
パターン4:AOPミドルウェアチェーン
AOP(アスペクト指向プログラミング)はデコレーターミドルウェアの最も強力な応用——ロギング、キャッシュ、認可、リトライ等の横断的関心事をビジネスロジックから分離し、ミドルウェアチェーンで合成実行する。
ミドルウェアコアアーキテクチャ
リクエストフロー
│
▼
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Auth │───▶│ Cache │───▶│ Retry │───▶│ Log │
│ミドルウェア│ │ミドルウェア│ │ミドルウェア│ │ミドルウェア│
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ ビジネス │
│ getUser() │
└──────────┘
│
▼
レスポンス
ミドルウェアインターフェース定義
interface MiddlewareContext {
className: string;
methodName: string;
args: any[];
result?: any;
error?: Error;
metadata: Map<string, any>;
startTime: number;
}
type NextFunction = () => Promise<any>;
interface Middleware {
name: string;
execute(context: MiddlewareContext, next: NextFunction): Promise<any>;
}
class MiddlewareChain {
private middlewares: Middleware[] = [];
use(middleware: Middleware): this {
this.middlewares.push(middleware);
return this;
}
async execute(
className: string,
methodName: string,
args: any[],
target: (...args: any[]) => any,
thisArg: any
): Promise<any> {
const context: MiddlewareContext = {
className,
methodName,
args,
metadata: new Map(),
startTime: performance.now(),
};
let index = 0;
const dispatch = async (): Promise<any> => {
if (index >= this.middlewares.length) {
context.result = await target.apply(thisArg, args);
return context.result;
}
const middleware = this.middlewares[index++];
return middleware.execute(context, dispatch);
};
return dispatch();
}
}
組み込みミドルウェア実装
class AuthMiddleware implements Middleware {
name = 'auth';
constructor(private permissionChecker: (method: string) => boolean) {}
async execute(context: MiddlewareContext, next: NextFunction): Promise<any> {
const hasPermission = this.permissionChecker(context.methodName);
if (!hasPermission) {
throw new Error(`[${context.className}.${context.methodName}] 権限不足`);
}
context.metadata.set('auth.checked', true);
return next();
}
}
class CacheMiddleware implements Middleware {
name = 'cache';
private cache = new Map<string, { value: any; expireAt: number }>();
constructor(private ttl: number = 60000) {}
async execute(context: MiddlewareContext, next: NextFunction): Promise<any> {
const cacheKey = `${context.className}:${context.methodName}:${JSON.stringify(context.args)}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expireAt > Date.now()) {
context.metadata.set('cache.hit', true);
return cached.value;
}
context.metadata.set('cache.hit', false);
const result = await next();
this.cache.set(cacheKey, {
value: result,
expireAt: Date.now() + this.ttl,
});
return result;
}
invalidate(pattern?: string): void {
if (!pattern) {
this.cache.clear();
return;
}
for (const key of this.cache.keys()) {
if (key.includes(pattern)) {
this.cache.delete(key);
}
}
}
}
class RetryMiddleware implements Middleware {
name = 'retry';
constructor(
private maxRetries: number = 3,
private delayMs: number = 1000,
private retryOn: (error: Error) => boolean = () => true
) {}
async execute(context: MiddlewareContext, next: NextFunction): Promise<any> {
let lastError: Error | undefined;
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
try {
const result = await next();
if (attempt > 1) {
context.metadata.set('retry.attempts', attempt);
}
return result;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < this.maxRetries && this.retryOn(lastError)) {
const delay = this.delayMs * Math.pow(2, attempt - 1);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw lastError;
}
}
throw lastError!;
}
}
class LoggingMiddleware implements Middleware {
name = 'logging';
async execute(context: MiddlewareContext, next: NextFunction): Promise<any> {
const startTime = performance.now();
const logPrefix = `[${context.className}.${context.methodName}]`;
console.log(`${logPrefix} 呼び出し開始`, {
args: context.args,
timestamp: new Date().toISOString(),
});
try {
const result = await next();
const duration = performance.now() - startTime;
console.log(`${logPrefix} 呼び出し成功`, {
duration: `${duration.toFixed(2)}ms`,
cacheHit: context.metadata.get('cache.hit'),
});
return result;
} catch (error) {
const duration = performance.now() - startTime;
console.error(`${logPrefix} 呼び出し失敗`, {
duration: `${duration.toFixed(2)}ms`,
error: error instanceof Error ? error.message : String(error),
retryAttempts: context.metadata.get('retry.attempts'),
});
throw error;
}
}
}
AOPデコレーター
const globalMiddlewareChain = new MiddlewareChain();
globalMiddlewareChain
.use(new AuthMiddleware((method) => true))
.use(new CacheMiddleware(30000))
.use(new RetryMiddleware(3, 1000))
.use(new LoggingMiddleware());
function UseMiddleware(chain?: MiddlewareChain) {
const middlewareChain = chain || globalMiddlewareChain;
return function (
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const originalMethod = target.value;
const methodName = String(context.name);
async function replacementMethod(this: any, ...args: any[]) {
return middlewareChain.execute(
this.constructor.name,
methodName,
args,
originalMethod,
this
);
}
return replacementMethod;
};
}
class UserService {
@UseMiddleware()
async getUser(id: string): Promise<{ id: string; name: string }> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`ユーザー取得失敗: ${response.status}`);
return response.json();
}
@UseMiddleware()
async createUser(data: { name: string; email: string }): Promise<string> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) throw new Error(`ユーザー作成失敗: ${response.status}`);
return response.json();
}
}
パターン5:依存性注入コンテナ
依存性注入(DI)はエンタープライズアプリケーションにおけるデコレーターミドルウェアの最も重要なパターン——デコレーターで依存関係を宣言し、コンテナが自動的に解決・注入する。
DIコンテナ実装
const INJECT_METADATA_KEY = Symbol('inject');
const INJECTABLE_METADATA_KEY = Symbol('injectable');
const SCOPE_METADATA_KEY = Symbol('scope');
enum InjectionScope {
Singleton = 'singleton',
Transient = 'transient',
Request = 'request',
}
interface InjectionToken<T = any> {
name: string;
__type?: T;
}
interface Provider<T = any> {
token: InjectionToken<T>;
useClass?: new (...args: any[]) => T;
useFactory?: (...args: any[]) => T | Promise<T>;
useValue?: T;
scope?: InjectionScope;
}
class DIContainer {
private providers = new Map<InjectionToken, Provider>();
private instances = new Map<InjectionToken, any>();
private factories = new Map<InjectionToken, (...args: any[]) => any>();
register<T>(provider: Provider<T>): this {
this.providers.set(provider.token, provider);
if (provider.useValue !== undefined) {
this.instances.set(provider.token, provider.useValue);
}
if (provider.useFactory) {
this.factories.set(provider.token, provider.useFactory);
}
return this;
}
async resolve<T>(token: InjectionToken<T>): Promise<T> {
const existingInstance = this.instances.get(token);
if (existingInstance !== undefined) {
return existingInstance;
}
const provider = this.providers.get(token);
if (!provider) {
throw new Error(`未登録の依存関係: ${token.name}`);
}
let instance: T;
if (provider.useClass) {
instance = await this.createInstance(provider.useClass);
} else if (provider.useFactory) {
const deps = await this.resolveFactoryDeps(provider.useFactory);
instance = await provider.useFactory(...deps);
} else {
throw new Error(`Provider ${token.name} に実装がありません`);
}
if (provider.scope !== InjectionScope.Transient) {
this.instances.set(token, instance);
}
return instance;
}
private async createInstance<T>(TargetClass: new (...args: any[]) => T): Promise<T> {
const injections: { index: number; token: InjectionToken }[] =
Reflect.getMetadata(INJECT_METADATA_KEY, TargetClass) || [];
const args: any[] = [];
for (const injection of injections) {
args[injection.index] = await this.resolve(injection.token);
}
return new TargetClass(...args);
}
private async resolveFactoryDeps(
factory: (...args: any[]) => any
): Promise<any[]> {
return [];
}
createScope(): DIContainer {
const scoped = new DIContainer();
for (const [token, provider] of this.providers) {
scoped.providers.set(token, provider);
}
for (const [token, instance] of this.instances) {
scoped.instances.set(token, instance);
}
return scoped;
}
}
const globalContainer = new DIContainer();
DIデコレーター
function Injectable(scope: InjectionScope = InjectionScope.Singleton) {
return function <T extends { new (...args: any[]): any }>(
target: T,
context: ClassDecoratorContext
) {
Reflect.defineMetadata(INJECTABLE_METADATA_KEY, true, target);
Reflect.defineMetadata(SCOPE_METADATA_KEY, scope, target);
const token: InjectionToken = { name: target.name };
globalContainer.register({
token,
useClass: target,
scope,
});
return target;
};
}
function Inject(token: InjectionToken) {
return function (
target: any,
context: ClassMethodDecoratorContext | ClassFieldDecoratorContext
) {
if (context.kind === 'method') {
const existingInjections: { index: number; token: InjectionToken }[] =
Reflect.getMetadata(INJECT_METADATA_KEY, target) || [];
existingInjections.push({ index: 0, token });
Reflect.defineMetadata(INJECT_METADATA_KEY, existingInjections, target);
}
};
}
function AutoInject(token?: InjectionToken) {
return function (
target: any,
context: ClassFieldDecoratorContext
) {
const resolvedToken = token || { name: String(context.name) };
return {
get(this: any) {
if (!this[`__injected_${String(context.name)}`]) {
this[`__injected_${String(context.name)}`] = globalContainer.resolve(resolvedToken);
}
return this[`__injected_${String(context.name)}`];
},
};
};
}
使用例
const TOKENS = {
Logger: { name: 'Logger' } as InjectionToken<LoggerService>,
Database: { name: 'Database' } as InjectionToken<DatabaseService>,
UserRepository: { name: 'UserRepository' } as InjectionToken<UserRepository>,
UserService: { name: 'UserService' } as InjectionToken<UserService>,
};
@Injectable(InjectionScope.Singleton)
class LoggerService {
log(message: string, ...args: any[]) {
console.log(`[Logger] ${message}`, ...args);
}
error(message: string, ...args: any[]) {
console.error(`[Logger] ${message}`, ...args);
}
}
@Injectable(InjectionScope.Singleton)
class DatabaseService {
private connected = false;
async connect(): Promise<void> {
this.connected = true;
console.log('[DB] データベース接続成功');
}
async query<T>(sql: string, params?: any[]): Promise<T[]> {
if (!this.connected) throw new Error('データベース未接続');
console.log(`[DB] クエリ実行: ${sql}`);
return [] as T[];
}
}
@Injectable(InjectionScope.Transient)
class UserRepository {
constructor(
private db: DatabaseService,
private logger: LoggerService
) {}
async findById(id: string) {
this.logger.log(`ユーザー検索: ${id}`);
return this.db.query('SELECT * FROM users WHERE id = $1', [id]);
}
}
@Injectable(InjectionScope.Singleton)
class UserService {
@AutoInject(TOKENS.UserRepository)
accessor userRepo!: UserRepository;
@AutoInject(TOKENS.Logger)
accessor logger!: LoggerService;
async getUser(id: string) {
this.logger.log(`ユーザー取得: ${id}`);
return this.userRepo.findById(id);
}
}
globalContainer.register({ token: TOKENS.Logger, useClass: LoggerService });
globalContainer.register({ token: TOKENS.Database, useClass: DatabaseService });
globalContainer.register({ token: TOKENS.UserRepository, useClass: UserRepository });
globalContainer.register({ token: TOKENS.UserService, useClass: UserService });
const userService = await globalContainer.resolve(TOKENS.UserService);
await userService.getUser('user-001');
パターン6:デコレーター合成&パイプラインパターン
デコレーターの最もエレガントな使い方は合成——複数のデコレーターがパイプライン順序で実行され、各デコレーターは一つのアスペクトにのみ注目し、組み合わせで複雑な機能を実現する。
パイプラインスタイルのデコレーター合成
interface PipeContext<TInput, TOutput> {
input: TInput;
output?: TOutput;
metadata: Map<string, any>;
skip?: boolean;
error?: Error;
}
type PipeHandler<TInput, TOutput> = (
context: PipeContext<TInput, TOutput>,
next: () => Promise<PipeContext<TInput, TOutput>>
) => Promise<PipeContext<TInput, TOutput>>;
class Pipeline<TInput, TOutput> {
private handlers: PipeHandler<TInput, TOutput>[] = [];
pipe(handler: PipeHandler<TInput, TOutput>): this {
this.handlers.push(handler);
return this;
}
async execute(input: TInput): Promise<TOutput> {
let context: PipeContext<TInput, TOutput> = {
input,
metadata: new Map(),
};
let index = 0;
const dispatch = async (): Promise<PipeContext<TInput, TOutput>> => {
if (index >= this.handlers.length || context.skip) {
return context;
}
const handler = this.handlers[index++];
return handler(context, dispatch);
};
context = await dispatch();
if (context.error) {
throw context.error;
}
return context.output as TOutput;
}
}
デコレーターパイプライン合成
function Timeout(ms: number) {
return function (
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const originalMethod = target.value;
async function replacementMethod(this: any, ...args: any[]) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), ms);
try {
const result = await Promise.race([
originalMethod.apply(this, args),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`メソッドタイムアウト: ${ms}ms`)), ms)
),
]);
clearTimeout(timeoutId);
return result;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
return replacementMethod;
};
}
function Debounce(ms: number) {
return function (
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const originalMethod = target.value;
let timeoutId: ReturnType<typeof setTimeout> | undefined;
function replacementMethod(this: any, ...args: any[]) {
if (timeoutId) {
clearTimeout(timeoutId);
}
return new Promise((resolve) => {
timeoutId = setTimeout(() => {
resolve(originalMethod.apply(this, args));
}, ms);
});
}
return replacementMethod;
};
}
function Throttle(ms: number) {
return function (
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const originalMethod = target.value;
let lastCallTime = 0;
function replacementMethod(this: any, ...args: any[]) {
const now = Date.now();
if (now - lastCallTime < ms) {
return undefined;
}
lastCallTime = now;
return originalMethod.apply(this, args);
}
return replacementMethod;
};
}
function Memoize(keyResolver?: (...args: any[]) => string) {
return function (
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const originalMethod = target.value;
const cache = new Map<string, { value: any; expireAt: number }>();
const defaultTtl = 5 * 60 * 1000;
async function replacementMethod(this: any, ...args: any[]) {
const key = keyResolver ? keyResolver(...args) : JSON.stringify(args);
const cached = cache.get(key);
if (cached && cached.expireAt > Date.now()) {
return cached.value;
}
const result = await originalMethod.apply(this, args);
cache.set(key, { value: result, expireAt: Date.now() + defaultTtl });
return result;
}
return replacementMethod;
};
}
合成使用例
class SearchService {
@logMinimal
@Timeout(5000)
@Memoize((query: string) => `search:${query}`)
async search(query: string): Promise<string[]> {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
return response.json();
}
@Debounce(300)
@logMinimal
async autoComplete(prefix: string): Promise<string[]> {
const response = await fetch(`/api/suggest?q=${encodeURIComponent(prefix)}`);
return response.json();
}
@Throttle(1000)
@logMinimal
trackSearch(query: string): void {
console.log(`検索トラッキング: ${query}`);
}
}
デコレーター実行順序
メソッド呼び出し → @logMinimal → @Timeout → @Memoize → 元のメソッド
│
メソッド戻り ← @logMinimal ← @Timeout ← @Memoize ← 元の結果
注意:TC39デコレーターのラップ順序は下から上(メソッドに最も近いものが先にラップ)
しかしランタイム呼び出しは外から内へ(最も外側のデコレーターが先にリクエストをインターセプト)
5つのよくある落とし穴と解決策
落とし穴1:デコレーター内でのthisコンテキスト喪失
// 誤った書き方
function badDecorator(
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
return function (...args: any[]) {
// thisコンテキストが喪失する可能性!
return target.value(...args);
};
}
// 正しい書き方
function goodDecorator(
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
return target.value.apply(this, args);
};
}
落とし穴2:非同期デコレーターでPromiseの返却を忘れる
// 誤った書き方
function badAsyncDecorator(
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
return function (this: any, ...args: any[]) {
const result = target.value.apply(this, args);
// Promiseの処理を忘れ、エラーがキャッチされない
console.log('完了');
return result;
};
}
// 正しい書き方
function goodAsyncDecorator(
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
return async function (this: any, ...args: any[]) {
try {
const result = await target.value.apply(this, args);
console.log('完了');
return result;
} catch (error) {
console.error('失敗', error);
throw error;
}
};
}
落とし穴3:デコレーターの順序エラーによる論理的混乱
// 誤った順序:キャッシュがバリデーションの前にあり、無効なデータをキャッシュする可能性
class BadService {
@Cache()
@Validate()
async getData(id: string) { /* ... */ }
}
// 正しい順序:バリデーションがキャッシュの前に
class GoodService {
@Validate()
@Cache()
async getData(id: string) { /* ... */ }
}
// 覚え方:Validate → Cache → Log → Business
落とし穴4:addInitializerでインスタンスプロパティにアクセス
// 誤った書き方:addInitializerはコンストラクタの前に実行される
function badInitDecorator(
target: any,
context: ClassFieldDecoratorContext
) {
context.addInitializer(function (this: any) {
// thisのプロパティはまだ初期化されていない可能性!
console.log(this.name);
});
}
// 正しい書き方:addInitializerではフックの登録のみ、インスタンスプロパティにはアクセスしない
function goodInitDecorator(
target: any,
context: ClassFieldDecoratorContext
) {
context.addInitializer(function (this: any) {
// メタデータの登録のみ、インスタンスプロパティにはアクセスしない
const existingInits: string[] = Reflect.getMetadata(
Symbol('inits'),
this.constructor
) || [];
existingInits.push(String(context.name));
Reflect.defineMetadata(Symbol('inits'), existingInits, this.constructor);
});
}
落とし穴5:LegacyデコレーターとTC39デコレーターの混用
// tsconfig.jsonで両方のモードを同時に有効にすることはできない
// 誤った設定:
{
"compilerOptions": {
"experimentalDecorators": true, // Legacyモード
"decorators": { "version": "2022-03" } // TC39モード
// これらは相互排他!どちらか一つを選択
}
}
// 正しい設定(TC39標準デコレーター):
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"experimentalDecorators": false
// TypeScript 5.8+はデフォルトでTC39標準デコレーターを使用
}
}
// マイグレーション戦略:ファイルごとに移行、混用しない
// 1. 新しいファイルはTC39デコレーターを使用
// 2. 古いファイルはLegacyを維持、eslintルールで制限
// 3. マイグレーション完了後、experimentalDecoratorsを無効化
10のよくあるエラートラブルシューティング
| エラーメッセージ | 原因 | 解決策 |
|---|---|---|
Decorators are not enabled |
tsconfigでデコレーターが有効化されていない | "experimentalDecorators": true(Legacy)を設定するかTS 5.8+にアップグレード |
Invalid decorator context kind |
デコレーターの種類がターゲットと一致しない | context.kindを確認、メソッドデコレーターがメソッドに使用されているか確認 |
Cannot read property 'value' of undefined |
Legacyデコレーターの構文をTC39で使用 | TC39デコレーターの最初の引数は{ value, ... }オブジェクト |
this is undefined in decorator |
アロー関数によるthisの喪失 | functionキーワードでreplacementMethodを定義 |
Decorators must return void or replace value |
デコレーターが無効な値を返した | メソッドデコレーターは関数を、クラスデコレーターはクラスを返す |
Metadata reflection not available |
reflect-metadataがインストールされていない | npm install reflect-metadataしてエントリポイントでインポート |
Decorator applied to constructor parameter |
TC39はパラメーターデコレーターをサポートしない | メソッドデコレーター+ランタイムバリデーションに変更 |
Circular dependency detected |
DIコンテナの循環依存 | LazyInjectを使用するか依存関係をリファクタリング |
Property 'addInitializer' does not exist |
TypeScriptバージョンが古い | TypeScript 5.0+にアップグレード |
Experimental feature warning |
Legacyデコレーターを使用している | TC39標準デコレーターに移行 |
高度な最適化テクニック
1. デコレーターパフォーマンス:遅延初期化
function LazyInit(factory: () => any) {
return function (
target: any,
context: ClassFieldDecoratorContext
) {
return {
get(this: any) {
const key = `__lazy_${String(context.name)}`;
if (!this[key]) {
this[key] = factory.call(this);
}
return this[key];
},
};
};
}
class ExpensiveService {
@LazyInit(() => new HeavyComputation())
accessor engine: any;
}
2. 条件付きデコレーター
function Conditional(
condition: () => boolean,
decorator: Function
) {
return function (...args: any[]) {
if (condition()) {
return (decorator as any)(...args);
}
return undefined;
};
}
const isProduction = () => process.env.NODE_ENV === 'production';
class ApiService {
@Conditional(isProduction, logMinimal)
async fetchData(url: string) {
return fetch(url).then((r) => r.json());
}
}
3. デコレーターメタデータシリアライズ
const SERIALIZABLE_KEY = Symbol('serializable');
interface SerializableField {
name: string;
type: string;
serializer?: (value: any) => any;
deserializer?: (value: any) => any;
}
function Serializable(options?: Partial<SerializableField>) {
return function (
target: any,
context: ClassFieldDecoratorContext
) {
context.addInitializer(function (this: any) {
const fields: SerializableField[] = Reflect.getMetadata(
SERIALIZABLE_KEY,
this.constructor
) || [];
fields.push({
name: String(context.name),
type: options?.type || 'string',
serializer: options?.serializer,
deserializer: options?.deserializer,
});
Reflect.defineMetadata(SERIALIZABLE_KEY, fields, this.constructor);
});
};
}
function toJSON(instance: any): Record<string, any> {
const fields: SerializableField[] = Reflect.getMetadata(
SERIALIZABLE_KEY,
instance.constructor
) || [];
const result: Record<string, any> = {};
for (const field of fields) {
const value = instance[field.name];
result[field.name] = field.serializer ? field.serializer(value) : value;
}
return result;
}
4. デコレーターデバッグツール
function DebugDecorator(
target: DecoratorTarget,
context: ClassMethodDecoratorContext
) {
const methodName = String(context.name);
return function (this: any, ...args: any[]) {
console.group(`🔍 ${this.constructor.name}.${methodName}`);
console.log('引数:', args);
console.trace('コールスタック');
try {
const result = target.value.apply(this, args);
if (result instanceof Promise) {
return result.then(
(value) => {
console.log('非同期結果:', value);
console.groupEnd();
return value;
},
(error) => {
console.error('非同期エラー:', error);
console.groupEnd();
throw error;
}
);
}
console.log('同期結果:', result);
console.groupEnd();
return result;
} catch (error) {
console.error('同期エラー:', error);
console.groupEnd();
throw error;
}
};
}
比較分析:TC39デコレーター vs Legacyデコレーター vs デコレーターファクトリ
| 特徴 | TC39標準デコレーター | Legacyデコレーター | デコレーターファクトリ |
|---|---|---|---|
| 仕様状態 | Stage 3.7、Stage 4に接近 | 非推奨、後方互換のみ | TC39ベースのファクトリパターン |
| TypeScriptサポート | 5.0+ネイティブ | experimentalDecoratorsが必要 |
TC39ベース |
| 実行順序 | 下から上(近いものが先にラップ) | 上から下 | TC39と同じ |
| 変更方式 | 新しい値を返して置換、元を変更しない | ディスクリプタを直接変更 | TC39と同じ |
| パラメーターデコレーター | サポートなし | サポートあり | サポートなし |
| メタデータサポート | 手動またはreflect-metadata | emitDecoratorMetadataが必要 |
手動 |
| thisバインディング | 安全(replacement関数経由) | 注意が必要 | 安全 |
| addInitializer | サポート(インスタンス化フック) | サポートなし | サポート |
| 不変性 | 高い(関数型置換) | 低い(可変変更) | 高い |
| エコシステム互換性 | 主要フレームワークが移行中 | 現在の主流 | 推奨 |
| 今後の方向性 | 標準仕様 | 段階的廃止 | ベストプラクティス |
マイグレーションチェックリスト
Legacy → TC39 マイグレーション手順:
□ 1. TypeScriptを5.0+にアップグレード
□ 2. experimentalDecoratorsを無効化
□ 3. デコレーターの引数を(target, key, descriptor)から(target, context)に変更
□ 4. descriptor.valueをtarget.valueに変更
□ 5. descriptor.get/setをtarget.get/setに変更
□ 6. context.addInitializerをconstructor注入の代わりに使用
□ 7. パラメーターデコレーターを削除、メソッドデコレーター+ランタイムバリデーションに変更
□ 8. reflect-metadataの使用方法を更新
□ 9. 完全なテストスイートを実行
□ 10. 段階的に移行、一度にすべてを変更しない
オンラインツール推奨
- JSONフォーマッター — デコレーターメタデータJSON出力のフォーマット
- コードフォーマッター — TypeScriptデコレーターコードのフォーマット
- 正規表現チートシート — バリデーションデコレーターで使用する正規表現パターン
関連記事
- TypeScriptランタイム型チェック実践:ZodからTypeBoxまで6つのプロダクションパターン — ランタイム型チェックとデコレーターバリデーションの完璧な組み合わせ
- TypeScript Zodバリデーション:ランタイムチェックから型安全まで5つのエンタープライズパターン — Zod Schemaとデコレーターバリデーションの統合ソリューション
- TypeScript Effectシステム:Effect-TSでエレガントに副作用を処理 — Effect-TSの依存性注入とデコレーターDIの比較
外部リファレンス
- TC39 Decorators Proposal — ECMAScriptデコレーター標準提案
- TypeScript 5.0 Release Notes - Decorators — TypeScript公式デコレータードキュメント
まとめ
TypeScript 5.8デコレーターミドルウェアは2026年に実験的機能からプロダクション対応へと移行した。TC39標準デコレーターの不変性、関数型置換、addInitializerフック等の特性により、デコレーターミドルウェアはAOPアーキテクチャ、依存性注入、パラメーターバリデーションを実現する強力なツールとなっている。
主要ポイントの振り返り:
- メソッドインターセプター:ロギング、タイミング、リトライ等の横断的関心事をビジネスコードに侵入させない
- クラストランスフォーマー:メタデータ登録、ルートマッピング、ORMマッピングをaddInitializer注入で実現
- パラメーターバリデーション:型チェック、スキーマバリデーションでメソッド実行前に不正入力をインターセプト
- AOPミドルウェアチェーン:Auth→Cache→Retry→Logのパイプライン実行で横断的関心事を分離
- 依存性注入:デコレーターで依存関係を宣言、コンテナが自動解決、Singleton/Transient/Requestスコープをサポート
- デコレーター合成:パイプラインパターンで複数のデコレーターを組み合わせ、各デコレーターは一つのアスペクトに集中
選択の推奨:新規プロジェクトはTC39標準デコレーターを直接使用し、既存プロジェクトは移行計画を策定して段階的に移行する。デコレーターミドルウェアのコアバリューは——ビジネスコードはビジネスに集中し、横断的関心事はデコレーターに任せることにある。
ブラウザローカルツールを無料で試す →