TypeScript 5.8デコレーターミドルウェア:メタプログラミングからAOPアーキテクチャまで6つのプロダクションパターン

编程语言

TypeScriptデコレーターがついにプロダクション対応に

まだexperimentalDecoratorsemitDecoratorMetadataを使っている?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. 段階的に移行、一度にすべてを変更しない

オンラインツール推奨

関連記事

外部リファレンス


まとめ

TypeScript 5.8デコレーターミドルウェアは2026年に実験的機能からプロダクション対応へと移行した。TC39標準デコレーターの不変性、関数型置換、addInitializerフック等の特性により、デコレーターミドルウェアはAOPアーキテクチャ、依存性注入、パラメーターバリデーションを実現する強力なツールとなっている。

主要ポイントの振り返り

  1. メソッドインターセプター:ロギング、タイミング、リトライ等の横断的関心事をビジネスコードに侵入させない
  2. クラストランスフォーマー:メタデータ登録、ルートマッピング、ORMマッピングをaddInitializer注入で実現
  3. パラメーターバリデーション:型チェック、スキーマバリデーションでメソッド実行前に不正入力をインターセプト
  4. AOPミドルウェアチェーン:Auth→Cache→Retry→Logのパイプライン実行で横断的関心事を分離
  5. 依存性注入:デコレーターで依存関係を宣言、コンテナが自動解決、Singleton/Transient/Requestスコープをサポート
  6. デコレーター合成:パイプラインパターンで複数のデコレーターを組み合わせ、各デコレーターは一つのアスペクトに集中

選択の推奨:新規プロジェクトはTC39標準デコレーターを直接使用し、既存プロジェクトは移行計画を策定して段階的に移行する。デコレーターミドルウェアのコアバリューは——ビジネスコードはビジネスに集中し、横断的関心事はデコレーターに任せることにある。

ブラウザローカルツールを無料で試す →

#TypeScript#装饰器#Decorator#AOP#元编程#中间件#2026#编程语言