TypeScript 5.8 Decorator Middleware: 6 Production Patterns from Metaprogramming to AOP Architecture

编程语言

TypeScript Decorators Are Finally Production-Ready

Still using experimentalDecorators with emitDecoratorMetadata? Sweating every TypeScript upgrade fearing breakage? In 2026, the TC39 decorator proposal has reached Stage 3.7, and TypeScript 5.8 natively supports standard decorators — no more experimental flags, no more semantic ambiguity, decorator middleware is finally safe for production. From method interceptors to AOP architecture, from parameter validation to dependency injection, decorators are redefining TypeScript's metaprogramming paradigm.

This article starts from TC39 decorator core concepts and guides you through method interceptors & logging decorators → class transformers & metadata registration → parameter validation decorators → AOP middleware chains → dependency injection containers → decorator composition & pipeline patterns with 6 production patterns, from spec to implementation.


Key Takeaways

  • The fundamental difference between TC39 standard decorators and Legacy decorators: replace/wrap instead of mutate
  • 3-layer decorator middleware architecture: Definition (Decorator) → Registration (Metadata) → Execution (Middleware Chain)
  • 6 production patterns covering logging, validation, AOP, DI, pipeline composition
  • 5 common pitfalls and solutions, 10 common error troubleshooting steps
  • Full comparison: TC39 Decorators vs Legacy Decorators vs Decorator Factories

Table of Contents

  • TC39 Decorator Core Concepts
  • Pattern 1: Method Interceptors & Logging Decorators
  • Pattern 2: Class Transformers & Metadata Registration
  • Pattern 3: Parameter Validation Decorators
  • Pattern 4: AOP Middleware Chains
  • Pattern 5: Dependency Injection Containers
  • Pattern 6: Decorator Composition & Pipeline Patterns
  • 5 Common Pitfalls & Solutions
  • 10 Common Error Troubleshooting
  • Advanced Optimization Tips
  • Comparison: TC39 Decorators vs Legacy Decorators vs Decorator Factories
  • Recommended Online Tools
  • Summary

TC39 Decorator Core Concepts

Concept Description
TC39 Decorator ECMAScript standard decorator proposal, Stage 3.7, natively supported in TypeScript 5.8
Method Decorator Intercepts method calls, can modify parameters/return values/exceptions
Class Decorator Replaces or enhances class definitions, can inject metadata
Accessor Decorator Intercepts getter/setter property access
Field Decorator Triggered during initialization, can replace initial values
Decorator Context Context object providing kind, name, access and other metadata
Add Initializer Initialization hook that runs automatically when a class is instantiated
Metadata Data attached to classes/methods via Symbols, used for DI, validation, etc.

TC39 Decorator Execution Flow

Definition Phase:
  @log                           Decorator factory invoked
  @validate                      Returns decorator function
  class UserService {            Decorators execute bottom-to-top
    @cache                       Method decorators execute first
    async getUser(id: string) {  Class decorator executes last
      ...
    }
  }

Runtime Phase:
  const instance = new UserService()  → Triggers addInitializer hooks
  instance.getUser('123')             → Triggers method decorator wrapper logic

Decorator Context (DecoratorContext):
  {
    kind: 'method' | 'class' | 'accessor' | 'field',
    name: 'getUser' | 'UserService',
    access: { get, set, has },
    addInitializer(fn) → Register initialization hook,
    static: boolean,
    private: boolean
  }

TC39 vs Legacy Decorator Core Differences

Legacy Decorators (experimentalDecorators):
  @log                              Mutates original descriptor
  class Svc {                       Order: top-to-bottom
    descriptor.value = wrappedFn    Directly mutates decorated object
  }

TC39 Standard Decorators:
  @log                              Returns replacement value
  class Svc {                       Order: bottom-to-top
    return { ...descriptor,         Does NOT mutate original, returns new
      method: wrappedFn }           Functional, immutable
  }

Pattern 1: Method Interceptors & Logging Decorators

Method interceptors are the most fundamental decorator middleware pattern — intercepting method calls without modifying the original code, adding cross-cutting concerns like logging, timing, and retries.

Basic Logging Decorator

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}] Call started`, {
      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}] Call succeeded`, {
              duration: `${duration.toFixed(2)}ms`,
              hasResult: resolvedValue !== undefined,
            });
            return resolvedValue;
          },
          (error) => {
            const duration = performance.now() - startTime;
            console.error(`[${className}.${methodName}] Async call failed`, {
              duration: `${duration.toFixed(2)}ms`,
              error: error instanceof Error ? error.message : String(error),
            });
            throw error;
          }
        );
      }

      const duration = performance.now() - startTime;
      console.log(`[${className}.${methodName}] Call succeeded`, {
        duration: `${duration.toFixed(2)}ms`,
      });
      return result;
    } catch (error) {
      const duration = performance.now() - startTime;
      console.error(`[${className}.${methodName}] Sync call failed`, {
        duration: `${duration.toFixed(2)}ms`,
        error: error instanceof Error ? error.message : String(error),
      });
      throw error;
    }
  }

  return replacementMethod;
}

Configurable Logging Decorator Factory

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} called`, 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} took ${duration.toFixed(2)}ms`,
                  resultLogData
                );
              } else {
                logFn(`[LOG] ${className}.${methodName} completed`, resultLogData);
              }
              return resolvedValue;
            },
            (error) => {
              const duration = performance.now() - startTime;
              console.error(`[ERROR] ${className}.${methodName} async failed`, {
                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} completed`, resultLogData);
        return result;
      } catch (error) {
        const duration = performance.now() - startTime;
        console.error(`[ERROR] ${className}.${methodName} sync failed`, {
          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,
});

Usage Example

class PaymentService {
  @logVerbose
  async processPayment(orderId: string, amount: number): Promise<string> {
    await new Promise((resolve) => setTimeout(resolve, 100));
    if (amount <= 0) {
      throw new Error('Amount must be greater than 0');
    }
    return `PAY-${orderId}-${Date.now()}`;
  }

  @logMinimal
  getPaymentStatus(paymentId: string): string {
    return 'completed';
  }
}

const paymentService = new PaymentService();
await paymentService.processPayment('ORD-001', 99.99);

Pattern 2: Class Transformers & Metadata Registration

Class decorators can replace entire class definitions or inject metadata via addInitializer during instantiation. This is the foundation for dependency injection, route registration, and ORM mapping.

Class Decorator Basics: Route Metadata Registration

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} registered with ${routes.length} routes`);
      }
    };
  };
}

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;
  };
}

Metadata Registration & Query

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} is required`);
        }
        if (fieldMeta.validator && !fieldMeta.validator(value)) {
          throw new Error(fieldMeta.errorMessage || `${fieldName} validation failed`);
        }
        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} is required`);
          }
          if (field.validator && !field.validator(value)) {
            errors.push(field.errorMessage || `${field.fieldName} validation failed`);
          }
        }
        return { valid: errors.length === 0, errors };
      }
    };
  };
}

Usage Example

@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: 'Invalid email format',
  })
  accessor email: string = '';

  @Field({ type: 'number', required: false })
  accessor age: number = 0;
}

const user = new User();
user.name = 'John';
user.email = 'john@example.com';
user.age = 28;

const result = user.validate();
console.log(result);

const schema = User.getValidationSchema();
console.log(schema);

Pattern 3: Parameter Validation Decorators

Parameter validation is one of the most practical decorator middleware scenarios — automatically validating parameter types and constraints before method execution, preventing runtime type errors.

Type Validation Decorator

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 = `param${i + 1}`;

        if (rule.required && (value === null || value === undefined)) {
          throw new TypeError(
            `[${this.constructor.name}.${methodName}] ${paramName} is required`
          );
        }

        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} type error: expected ${rule.type}, got ${actualType}`
            );
          }

          if (rule.type === 'string') {
            if (rule.minLength && value.length < rule.minLength) {
              throw new RangeError(
                `[${this.constructor.name}.${methodName}] ${paramName} min length ${rule.minLength}`
              );
            }
            if (rule.maxLength && value.length > rule.maxLength) {
              throw new RangeError(
                `[${this.constructor.name}.${methodName}] ${paramName} max length ${rule.maxLength}`
              );
            }
            if (rule.pattern && !rule.pattern.test(value)) {
              throw new Error(
                rule.message || `[${this.constructor.name}.${methodName}] ${paramName} invalid format`
              );
            }
          }

          if (rule.type === 'number') {
            if (rule.min !== undefined && value < rule.min) {
              throw new RangeError(
                `[${this.constructor.name}.${methodName}] ${paramName} min value ${rule.min}`
              );
            }
            if (rule.max !== undefined && value > rule.max) {
              throw new RangeError(
                `[${this.constructor.name}.${methodName}] ${paramName} max value ${rule.max}`
              );
            }
          }

          if (rule.custom && !rule.custom(value)) {
            throw new Error(
              rule.message || `[${this.constructor.name}.${methodName}] ${paramName} validation failed`
            );
          }
        }
      }

      return target.value.apply(this, args);
    }

    return replacementMethod;
  };
}

Schema Validation Decorator (Zod Integration)

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}] param${i + 1} validation failed: ${errors.join('; ')}`
          );
        }

        args[i] = result.data;
      }

      return target.value.apply(this, args);
    }

    return replacementMethod;
  };
}

Usage Example

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: 'Order ID format: XX-0000' },
    { type: 'number', required: true, min: 0, max: 1000000 }
  )
  createOrder(orderId: string, amount: number): string {
    return `Order ${orderId} created, amount ${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);
}

Pattern 4: AOP Middleware Chains

AOP (Aspect-Oriented Programming) is the most powerful application of decorator middleware — decoupling cross-cutting concerns like logging, caching, authorization, and retries from business logic through composable middleware chains.

Middleware Core Architecture

Request Flow
   │
   ▼
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  Auth    │───▶│  Cache   │───▶│  Retry   │───▶│  Log     │
│Middleware│    │Middleware│    │Middleware│    │Middleware│
└──────────┘    └──────────┘    └──────────┘    └──────────┘
                                                   │
                                                   ▼
                                            ┌──────────┐
                                            │ Business │
                                            │ getUser() │
                                            └──────────┘
                                                   │
                                                   ▼
                                              Response

Middleware Interface Definition

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();
  }
}

Built-in Middleware Implementations

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}] Permission denied`);
    }
    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} call started`, {
      args: context.args,
      timestamp: new Date().toISOString(),
    });

    try {
      const result = await next();
      const duration = performance.now() - startTime;
      console.log(`${logPrefix} call succeeded`, {
        duration: `${duration.toFixed(2)}ms`,
        cacheHit: context.metadata.get('cache.hit'),
      });
      return result;
    } catch (error) {
      const duration = performance.now() - startTime;
      console.error(`${logPrefix} call failed`, {
        duration: `${duration.toFixed(2)}ms`,
        error: error instanceof Error ? error.message : String(error),
        retryAttempts: context.metadata.get('retry.attempts'),
      });
      throw error;
    }
  }
}

AOP Decorator

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(`Failed to fetch user: ${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(`Failed to create user: ${response.status}`);
    return response.json();
  }
}

Pattern 5: Dependency Injection Containers

Dependency Injection (DI) is the most critical decorator middleware pattern in enterprise applications — declaring dependencies through decorators while the container automatically resolves and injects instances.

DI Container Implementation

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(`Unregistered dependency: ${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} missing implementation`);
    }

    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 Decorators

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)}`];
      },
    };
  };
}

Usage Example

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] Database connected');
  }

  async query<T>(sql: string, params?: any[]): Promise<T[]> {
    if (!this.connected) throw new Error('Database not connected');
    console.log(`[DB] Execute query: ${sql}`);
    return [] as T[];
  }
}

@Injectable(InjectionScope.Transient)
class UserRepository {
  constructor(
    private db: DatabaseService,
    private logger: LoggerService
  ) {}

  async findById(id: string) {
    this.logger.log(`Find user: ${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(`Get user: ${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');

Pattern 6: Decorator Composition & Pipeline Patterns

The most elegant use of decorators is composition — multiple decorators execute in pipeline order, each focusing on a single aspect, achieving complex functionality through combination.

Pipeline-style Decorator Composition

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;
  }
}

Decorator Pipeline Composition

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(`Method timeout: ${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;
  };
}

Composition Usage Example

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(`Search tracked: ${query}`);
  }
}

Decorator Execution Order

Method call → @logMinimal → @Timeout → @Memoize → Original method
                                                        │
Method return ← @logMinimal ← @Timeout ← @Memoize ← Original result

Note: TC39 decorator wrapping order is bottom-to-top (closest to method wraps first)
But runtime invocation is outside-in (outermost decorator intercepts first)

5 Common Pitfalls & Solutions

Pitfall 1: Lost this Context in Decorators

// Wrong
function badDecorator(
  target: DecoratorTarget,
  context: ClassMethodDecoratorContext
) {
  return function (...args: any[]) {
    // this context may be lost!
    return target.value(...args);
  };
}

// Correct
function goodDecorator(
  target: DecoratorTarget,
  context: ClassMethodDecoratorContext
) {
  return function (this: any, ...args: any[]) {
    return target.value.apply(this, args);
  };
}

Pitfall 2: Forgetting to Return Promise in Async Decorators

// Wrong
function badAsyncDecorator(
  target: DecoratorTarget,
  context: ClassMethodDecoratorContext
) {
  return function (this: any, ...args: any[]) {
    const result = target.value.apply(this, args);
    // Forgot to handle Promise, errors won't be caught
    console.log('Done');
    return result;
  };
}

// Correct
function goodAsyncDecorator(
  target: DecoratorTarget,
  context: ClassMethodDecoratorContext
) {
  return async function (this: any, ...args: any[]) {
    try {
      const result = await target.value.apply(this, args);
      console.log('Done');
      return result;
    } catch (error) {
      console.error('Failed', error);
      throw error;
    }
  };
}

Pitfall 3: Wrong Decorator Order Causing Logic Issues

// Wrong order: cache before validation, may cache invalid data
class BadService {
  @Cache()
  @Validate()
  async getData(id: string) { /* ... */ }
}

// Correct order: validation before cache
class GoodService {
  @Validate()
  @Cache()
  async getData(id: string) { /* ... */ }
}

// Mnemonic: Validate → Cache → Log → Business

Pitfall 4: Accessing Instance Properties in addInitializer

// Wrong: addInitializer runs before the constructor
function badInitDecorator(
  target: any,
  context: ClassFieldDecoratorContext
) {
  context.addInitializer(function (this: any) {
    // Instance properties may not be initialized yet!
    console.log(this.name);
  });
}

// Correct: Only register hooks in addInitializer, don't access instance properties
function goodInitDecorator(
  target: any,
  context: ClassFieldDecoratorContext
) {
  context.addInitializer(function (this: any) {
    // Only register metadata, don't access instance properties
    const existingInits: string[] = Reflect.getMetadata(
      Symbol('inits'),
      this.constructor
    ) || [];
    existingInits.push(String(context.name));
    Reflect.defineMetadata(Symbol('inits'), existingInits, this.constructor);
  });
}

Pitfall 5: Mixing Legacy and TC39 Decorators

// tsconfig.json cannot enable both modes simultaneously
// Wrong config:
{
  "compilerOptions": {
    "experimentalDecorators": true,  // Legacy mode
    "decorators": { "version": "2022-03" }  // TC39 mode
    // These are mutually exclusive! Pick one
  }
}

// Correct config (TC39 standard decorators):
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "experimentalDecorators": false
    // TypeScript 5.8+ uses TC39 standard decorators by default
  }
}

// Migration strategy: migrate file by file, don't mix
// 1. New files use TC39 decorators
// 2. Old files keep Legacy, enforce via eslint rules
// 3. After migration complete, disable experimentalDecorators

10 Common Error Troubleshooting

Error Message Cause Solution
Decorators are not enabled Decorators not enabled in tsconfig Set "experimentalDecorators": true (Legacy) or upgrade to TS 5.8+
Invalid decorator context kind Decorator type doesn't match target Check context.kind, ensure method decorator is used on methods
Cannot read property 'value' of undefined Legacy decorator syntax used with TC39 TC39 decorator first param is { value, ... } object
this is undefined in decorator Arrow function causing lost this Use function keyword for replacementMethod
Decorators must return void or replace value Decorator returned invalid value Method decorator returns function, class decorator returns class
Metadata reflection not available reflect-metadata not installed npm install reflect-metadata and import at entry point
Decorator applied to constructor parameter TC39 doesn't support parameter decorators Use method decorator + runtime validation instead
Circular dependency detected DI container circular dependency Use LazyInject or refactor dependency relationships
Property 'addInitializer' does not exist TypeScript version too old Upgrade to TypeScript 5.0+
Experimental feature warning Using Legacy decorators Migrate to TC39 standard decorators

Advanced Optimization Tips

1. Decorator Performance: Lazy Initialization

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. Conditional Decorators

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. Decorator Metadata Serialization

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. Decorator Debugging Tool

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('Arguments:', args);
    console.trace('Call stack');

    try {
      const result = target.value.apply(this, args);
      if (result instanceof Promise) {
        return result.then(
          (value) => {
            console.log('Async result:', value);
            console.groupEnd();
            return value;
          },
          (error) => {
            console.error('Async error:', error);
            console.groupEnd();
            throw error;
          }
        );
      }

      console.log('Sync result:', result);
      console.groupEnd();
      return result;
    } catch (error) {
      console.error('Sync error:', error);
      console.groupEnd();
      throw error;
    }
  };
}

Comparison: TC39 Decorators vs Legacy Decorators vs Decorator Factories

Feature TC39 Standard Decorators Legacy Decorators Decorator Factories
Spec Status Stage 3.7, approaching Stage 4 Deprecated, backward-compat only Factory pattern based on TC39
TypeScript Support 5.0+ native Requires experimentalDecorators Based on TC39
Execution Order Bottom-to-top (closest wraps first) Top-to-bottom Same as TC39
Mutation Style Returns new value, doesn't mutate original Directly mutates descriptor Same as TC39
Parameter Decorators Not supported Supported Not supported
Metadata Support Manual or reflect-metadata Requires emitDecoratorMetadata Manual
this Binding Safe (via replacement function) Needs attention Safe
addInitializer Supported (instantiation hook) Not supported Supported
Immutability High (functional replacement) Low (mutable modification) High
Ecosystem Compat Major frameworks migrating Current mainstream Recommended
Future Direction Standard specification Gradual deprecation Best practice

Migration Checklist

Legacy → TC39 Migration Steps:
□ 1. Upgrade TypeScript to 5.0+
□ 2. Disable experimentalDecorators
□ 3. Change decorator params from (target, key, descriptor) to (target, context)
□ 4. Change descriptor.value to target.value
□ 5. Change descriptor.get/set to target.get/set
□ 6. Use context.addInitializer instead of constructor injection
□ 7. Remove parameter decorators, use method decorators + runtime validation
□ 8. Update reflect-metadata usage
□ 9. Run full test suite
□ 10. Migrate incrementally, don't change everything at once

External References


Summary

TypeScript 5.8 decorator middleware has moved from experimental to production-ready in 2026. TC39 standard decorators' immutability, functional replacement, and addInitializer hooks make decorator middleware a powerful tool for implementing AOP architecture, dependency injection, and parameter validation.

Key Takeaways:

  1. Method Interceptors: Logging, timing, retries as cross-cutting concerns without invading business code
  2. Class Transformers: Metadata registration, route mapping, ORM mapping via addInitializer injection
  3. Parameter Validation: Type checking, Schema validation, intercepting invalid inputs before method execution
  4. AOP Middleware Chains: Auth→Cache→Retry→Log pipeline execution, decoupling cross-cutting concerns
  5. Dependency Injection: Decorator-declared dependencies with automatic container resolution, supporting Singleton/Transient/Request scopes
  6. Decorator Composition: Pipeline pattern combining multiple decorators, each focused on a single aspect

Recommendation: New projects should use TC39 standard decorators directly; existing projects should create a migration plan for gradual transition. The core value of decorator middleware is — let business code focus on business, and let decorators handle cross-cutting concerns.

Try these browser-local tools — no sign-up required →

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