TypeScript 5 Decorators & Metaprogramming: From Principles to a Complete Dependency Injection Framework

前端工程(Updated Jun 2, 2026)

The Evolution of Decorators

Stage 2 (Legacy, TypeScript 4)

// Legacy: decorator is a function that receives (target, key, descriptor)
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${key} with`, args);
    return original.apply(this, args);
  };
}

Stage 3 (New, TypeScript 5+)

// New: decorator is a function that returns a function
function Log(
  value: Function,
  context: ClassMethodDecoratorContext
) {
  return function (this: any, ...args: any[]) {
    console.log(`Calling ${String(context.name)} with`, args);
    return value.apply(this, args);
  };
}

Key Differences

Aspect Stage 2 (Legacy) Stage 3 (New)
Parameters (target, key, descriptor) (value, context)
Return Value Modify descriptor Return replacement value
Metadata Requires reflect-metadata Native context.metadata
Class Decorator Modify constructor Return new constructor
Execution Order Inside to outside Outside to inside
TC39 Status Deprecated Stage 3

Five Decorator Types

1. Class Decorators

function sealed<T extends { new(...args: any[]): {} }>(
  target: T,
  context: ClassDecoratorContext
) {
  Object.seal(target);
  Object.seal(target.prototype);
  return target;
}

@sealed
class Config {
  static apiKey = 'xxx';
}

2. Method Decorators

function debounce(delay: number) {
  return function (
    originalMethod: Function,
    context: ClassMethodDecoratorContext
  ) {
    let timer: ReturnType<typeof setTimeout>;

    return function (this: any, ...args: any[]) {
      clearTimeout(timer);
      timer = setTimeout(() => originalMethod.apply(this, args), delay);
    };
  };
}

class SearchBox {
  @debounce(300)
  onInput(text: string) {
    fetchSuggestions(text);
  }
}

3. Property Decorators

function required(
  value: undefined,
  context: ClassFieldDecoratorContext
) {
  return function (this: any, initialValue: any) {
    if (initialValue === undefined || initialValue === null) {
      throw new Error(`${String(context.name)} is required`);
    }
    return initialValue;
  };
}

class User {
  @required name!: string;
  @required email!: string;
}

4. Getter/Setter Decorators

function format(template: string) {
  return function (
    target: Function,
    context: ClassGetterDecoratorContext
  ) {
    return function (this: any) {
      const raw = target.call(this);
      return template.replace('{}', String(raw));
    };
  };
}

class Product {
  #price: number;

  @format('${}')
  get price() { return this.#price; }
}

5. Auto-Accessor Decorators

function logged(
  target: ClassAccessorDecoratorTarget<any, any>,
  context: ClassAccessorDecoratorContext
): ClassAccessorDecoratorResult<any, any> {
  return {
    get(this: any) {
      const value = target.get.call(this);
      console.log(`Getting ${String(context.name)}: ${value}`);
      return value;
    },
    set(this: any, newValue: any) {
      console.log(`Setting ${String(context.name)}: ${newValue}`);
      target.set.call(this, newValue);
    },
  };
}

class Settings {
  @logged accessor theme = 'light';
}

Metadata Reflection

reflect-metadata

import 'reflect-metadata';

// Define metadata
Reflect.defineMetadata('role', 'admin', UserController);
Reflect.defineMetadata('route', '/api/users', UserController.prototype, 'getUsers');

// Read metadata
const role = Reflect.getMetadata('role', UserController); // 'admin'
const route = Reflect.getMetadata('route', UserController.prototype, 'getUsers'); // '/api/users'

Decorators + Metadata = Declarative Programming

import 'reflect-metadata';

const ROUTE_KEY = Symbol('route');
const METHOD_KEY = Symbol('method');

function Get(path: string) {
  return function (target: Function, context: ClassMethodDecoratorContext) {
    Reflect.defineMetadata(ROUTE_KEY, path, context.static);
    Reflect.defineMetadata(METHOD_KEY, 'GET', context.static);
  };
}

function Controller(prefix: string) {
  return function (target: Function, context: ClassDecoratorContext) {
    Reflect.defineMetadata('prefix', prefix, target);
  };
}

@Controller('/api/users')
class UserController {
  @Get('/')
  list() { return [{ id: 1, name: 'Alice' }]; }

  @Get('/:id')
  detail() { return { id: 1, name: 'Alice' }; }
}

// Auto-register routes
function registerRoutes(controller: any) {
  const prefix = Reflect.getMetadata('prefix', controller.constructor);
  const methods = Object.getOwnPropertyNames(controller.constructor.prototype);

  methods.forEach(method => {
    const route = Reflect.getMetadata(ROUTE_KEY, controller.constructor.prototype, method);
    if (route) {
      const fullPath = prefix + route;
      console.log(`Register: GET ${fullPath} → ${method}`);
    }
  });
}

Hands-On: Building an IoC Container from Scratch

import 'reflect-metadata';

const INJECT_KEY = Symbol('inject');

// @Injectable decorator: marks injectable services
function Injectable(target: Function, context: ClassDecoratorContext) {
  Reflect.defineMetadata('injectable', true, target);
  return target;
}

// @Inject decorator: declares dependencies
function Inject(token: string) {
  return function (value: undefined, context: ClassFieldDecoratorContext) {
    return function (this: any) {
      return Container.resolve(token);
    };
  };
}

// IoC Container
class Container {
  private static instances = new Map<string, any>();
  private static registrations = new Map<string, Function>();

  static register(token: string, target: Function) {
    this.registrations.set(token, target);
  }

  static resolve<T>(token: string): T {
    if (this.instances.has(token)) {
      return this.instances.get(token);
    }

    const Target = this.registrations.get(token);
    if (!Target) throw new Error(`No registration for ${token}`);

    const instance = new (Target as any)();
    this.instances.set(token, instance);
    return instance;
  }
}

// Usage
@Injectable
class Logger {
  log(msg: string) { console.log(`[LOG] ${msg}`); }
}

@Injectable
class Database {
  @Inject('Logger') logger!: Logger;

  query(sql: string) {
    this.logger.log(`Query: ${sql}`);
    return [{ id: 1 }];
  }
}

Container.register('Logger', Logger);
Container.register('Database', Database);

const db = Container.resolve<Database>('Database');
db.query('SELECT * FROM users');

Hands-On: ORM Property Mapping

import 'reflect-metadata';

const COLUMN_KEY = Symbol('column');

function Column(name: string, options?: { type?: string; primary?: boolean }) {
  return function (value: undefined, context: ClassFieldDecoratorContext) {
    Reflect.defineMetadata(COLUMN_KEY, { name, ...options }, context.static);
  };
}

function Entity(table: string) {
  return function (target: Function, context: ClassDecoratorContext) {
    Reflect.defineMetadata('table', table, target);
  };
}

@Entity('users')
class User {
  @Column('id', { primary: true })
  id!: number;

  @Column('username')
  name!: string;

  @Column('created_at', { type: 'datetime' })
  createdAt!: Date;
}

// Auto-generate SQL
function toSQL(entity: Function): string {
  const table = Reflect.getMetadata('table', entity);
  const fields: string[] = [];

  const prototype = entity.prototype;
  for (const key of Object.getOwnPropertyNames(prototype)) {
    const col = Reflect.getMetadata(COLUMN_KEY, prototype, key);
    if (col) {
      fields.push(`${col.name} ${col.type || 'VARCHAR(255)'}`);
    }
  }

  return `CREATE TABLE ${table} (${fields.join(', ')});`;
}

Hands-On: Validation Framework

import 'reflect-metadata';

const VALIDATE_KEY = Symbol('validate');

function validate(validator: (value: any) => boolean, message: string) {
  return function (value: undefined, context: ClassFieldDecoratorContext) {
    const existing = Reflect.getMetadata(VALIDATE_KEY, context.static) || [];
    existing.push({ validator, message, field: context.name });
    Reflect.defineMetadata(VALIDATE_KEY, existing, context.static);
  };
}

const Min = (n: number) => validate(v => v >= n, `Must be at least ${n}`);
const Max = (n: number) => validate(v => v <= n, `Must be at most ${n}`);
const Email = () => validate(v => /^[^@]+@[^@]+$/.test(v), 'Invalid email');
const Required = () => validate(v => v != null, 'Is required');

class CreateUserDTO {
  @Required() @Min(1) id!: number;
  @Required() @Email() email!: string;
  @Required() @Min(0) @Max(150) age!: number;
}

function validateDto(instance: any): string[] {
  const rules = Reflect.getMetadata(VALIDATE_KEY, instance.constructor) || [];
  const errors: string[] = [];

  for (const rule of rules) {
    const value = instance[rule.field];
    if (!rule.validator(value)) {
      errors.push(`${String(rule.field)} ${rule.message}`);
    }
  }

  return errors;
}

Decorator Factory Pattern

// Decorator factory: receives parameters, returns a decorator
function Cache(ttl: number) {
  const cache = new Map<string, { value: any; expiry: number }>();

  return function (originalMethod: Function, context: ClassMethodDecoratorContext) {
    return function (this: any, ...args: any[]) {
      const key = JSON.stringify(args);
      const cached = cache.get(key);

      if (cached && Date.now() < cached.expiry) {
        return cached.value;
      }

      const result = originalMethod.apply(this, args);
      cache.set(key, { value: result, expiry: Date.now() + ttl * 1000 });
      return result;
    };
  };
}

class ApiService {
  @Cache(60) // Cache for 60 seconds
  async getUsers() {
    return fetch('/api/users').then(r => r.json());
  }
}

TypeScript Configuration

{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": false,
    "emitDecoratorMetadata": true
  }
}
Option Description
experimentalDecorators: false Use Stage 3 new decorators
experimentalDecorators: true Use legacy decorators
emitDecoratorMetadata: true Emit type metadata (requires reflect-metadata)

Summary

TypeScript 5's Stage 3 decorators represent a standardization milestone for metaprogramming. Combined with reflect-metadata, decorators become a powerful tool for declarative programming — IoC containers, ORM mappings, validation frameworks, and route registration can all be elegantly implemented with decorators. The core idea: declare "what to do" in your code, and encapsulate "how to do it" inside the decorator.

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

#TypeScript#装饰器#元编程#Reflect.metadata#依赖注入