TypeScript 5 裝飾器與元程式設計:從原理到依賴注入框架的完整實作

前端工程(更新於 2026年6月2日)

裝飾器的前世今生

Stage 2(舊版,TypeScript 4)

// 舊版:裝飾器是一個函數,接收 (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(新版,TypeScript 5+)

// 新版:裝飾器是一個函數,返回一個函數
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);
  };
}

關鍵差異

維度 Stage 2(舊) Stage 3(新)
參數 (target, key, descriptor) (value, context)
返回值 修改 descriptor 返回替換值
元資料 需要 reflect-metadata 原生 context.metadata
類別裝飾器 修改建構函式 返回新建構函式
執行順序 從內到外 從外到內
TC39 進度 廢棄 Stage 3

五種裝飾器類型

1. 類別裝飾器

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. 方法裝飾器

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. 屬性裝飾器

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 裝飾器

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)

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

元資料反射

reflect-metadata

import 'reflect-metadata';

// 定義元資料
Reflect.defineMetadata('role', 'admin', UserController);
Reflect.defineMetadata('route', '/api/users', UserController.prototype, 'getUsers');

// 讀取元資料
const role = Reflect.getMetadata('role', UserController); // 'admin'
const route = Reflect.getMetadata('route', UserController.prototype, 'getUsers'); // '/api/users'

裝飾器 + 元資料 = 宣告式程式設計

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

// 自動註冊路由
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}`);
    }
  });
}

實戰:從零實作 IoC 容器

import 'reflect-metadata';

const INJECT_KEY = Symbol('inject');

// @Injectable 裝飾器:標記可注入的服務
function Injectable(target: Function, context: ClassDecoratorContext) {
  Reflect.defineMetadata('injectable', true, target);
  return target;
}

// @Inject 裝飾器:宣告依賴
function Inject(token: string) {
  return function (value: undefined, context: ClassFieldDecoratorContext) {
    return function (this: any) {
      return Container.resolve(token);
    };
  };
}

// IoC 容器
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;
  }
}

// 使用
@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');

實戰:ORM 屬性映射

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

// 自動生成 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(', ')});`;
}

實戰:驗證框架

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

裝飾器工廠模式

// 裝飾器工廠:接收參數,返回裝飾器
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) // 快取 60 秒
  async getUsers() {
    return fetch('/api/users').then(r => r.json());
  }
}

TypeScript 設定

{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": false,
    "emitDecoratorMetadata": true
  }
}
選項 說明
experimentalDecorators: false 使用 Stage 3 新裝飾器
experimentalDecorators: true 使用舊版裝飾器
emitDecoratorMetadata: true 發出型別元資料(需要 reflect-metadata

總結

TypeScript 5 的 Stage 3 裝飾器是元程式設計的標準化里程碑。配合 reflect-metadata,裝飾器成為宣告式程式設計的利器——IoC 容器、ORM 映射、驗證框架、路由註冊,都可以用裝飾器優雅實作。核心思想:把「做什麼」宣告在程式碼上,把「怎麼做」封裝在裝飾器裡

本站提供瀏覽器本地工具,免註冊即可試用 →

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