强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

TypeScript 开发指南 / 13 - 装饰器

装饰器

装饰器(Decorators)是一种特殊的声明,可以附加到类声明、方法、属性或参数上,用于修改或扩展它们的行为。

注意:TypeScript 5.0+ 支持 ECMAScript 标准装饰器。本文档介绍的是标准装饰器语法。旧版实验性装饰器(experimentalDecorators)在 NestJS 等框架中仍广泛使用。

启用装饰器

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": true  // 旧版实验性装饰器
  }
}

TypeScript 5.0+ 的标准装饰器不需要特殊配置,只需 target: "ES2022" 或更高。

类装饰器(Class Decorators)

// 类装饰器接收类构造函数作为参数
function Sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@Sealed
class Greeter {
  greeting: string;

  constructor(message: string) {
    this.greeting = message;
  }

  greet() {
    return `Hello, ${this.greeting}`;
  }
}

// 装饰器工厂(返回装饰器函数的函数)
function Logger(prefix: string) {
  return function (constructor: Function) {
    console.log(`[${prefix}] Creating instance of ${constructor.name}`);
  };
}

@Logger("APP")
class User {
  constructor(public name: string) {}
}

// 输出: [APP] Creating instance of User

类装饰器的返回值

// 返回新类来替换原始类
function Mixin(...mixins: any[]) {
  return function <T extends new (...args: any[]) => any>(constructor: T) {
    return class extends constructor {
      constructor(...args: any[]) {
        super(...args);
        mixins.forEach(mixin => {
          Object.assign(this, new mixin());
        });
      }
    };
  };
}

class Serializable {
  serialize() {
    return JSON.stringify(this);
  }
}

@Mixin(Serializable)
class User {
  constructor(public name: string) {}
}

const user = new User("Alice");
console.log(user.serialize()); // '{"name":"Alice"}'

方法装饰器(Method Decorators)

// 方法装饰器参数:
// target: 对于静态成员是类构造函数,对于实例成员是类原型
// propertyKey: 方法名
// descriptor: 属性描述符
function Log(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${propertyKey} with args:`, args);
    const result = originalMethod.apply(this, args);
    console.log(`${propertyKey} returned:`, result);
    return result;
  };
}

class Calculator {
  @Log
  add(a: number, b: number): number {
    return a + b;
  }

  @Log
  multiply(a: number, b: number): number {
    return a * b;
  }
}

const calc = new Calculator();
calc.add(1, 2);
// Calling add with args: [1, 2]
// add returned: 3

方法装饰器的返回值

// 返回新的 PropertyDescriptor
function Throttle(delay: number) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    let lastCall = 0;
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      const now = Date.now();
      if (now - lastCall >= delay) {
        lastCall = now;
        return originalMethod.apply(this, args);
      }
    };

    return descriptor;
  };
}

class SearchService {
  @Throttle(300)
  search(query: string) {
    console.log(`Searching: ${query}`);
  }
}

属性装饰器(Property Decorators)

// 属性装饰器参数:
// target: 对于静态成员是类构造函数,对于实例成员是类原型
// propertyKey: 属性名
function Required(target: any, propertyKey: string) {
  let value: any;

  const getter = () => value;
  const setter = (newVal: any) => {
    if (newVal === undefined || newVal === null) {
      throw new Error(`${propertyKey} is required`);
    }
    value = newVal;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true
  });
}

class User {
  @Required
  name!: string;

  @Required
  email!: string;

  age?: number;
}

const user = new User();
user.name = "Alice";   // ✅
// user.name = null!;  // ❌ Error: name is required

参数装饰器(Parameter Decorators)

// 参数装饰器参数:
// target: 类原型
// propertyKey: 方法名
// parameterIndex: 参数索引
function Validate(
  target: any,
  propertyKey: string,
  parameterIndex: number
) {
  const existingValidators: number[] =
    Reflect.getOwnMetadata("validators", target, propertyKey) || [];
  existingValidators.push(parameterIndex);
  Reflect.defineMetadata("validators", existingValidators, target, propertyKey);
}

class UserService {
  createUser(@Validate name: string, @Validate email: string, age?: number) {
    // ...
  }
}

装饰器组合与执行顺序

function First() {
  console.log("First(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("First(): called");
  };
}

function Second() {
  console.log("Second(): factory evaluated");
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    console.log("Second(): called");
  };
}

class Example {
  @First()
  @Second()
  method() {}
}

// 执行顺序:
// First(): factory evaluated
// Second(): factory evaluated
// Second(): called
// First(): called
// 从下往上执行

实用装饰器示例

Memoize 装饰器

function Memoize(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  const cache = new Map<string, any>();

  descriptor.value = function (...args: any[]) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = originalMethod.apply(this, args);
    cache.set(key, result);
    return result;
  };

  return descriptor;
}

class MathService {
  @Memoize
  fibonacci(n: number): number {
    if (n <= 1) return n;
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

Retry 装饰器

function Retry(maxAttempts: number = 3, delay: number = 1000) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      let lastError: Error;
      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        try {
          return await originalMethod.apply(this, args);
        } catch (error) {
          lastError = error as Error;
          if (attempt < maxAttempts) {
            await new Promise(r => setTimeout(r, delay * attempt));
          }
        }
      }
      throw lastError!;
    };

    return descriptor;
  };
}

class ApiService {
  @Retry(3, 1000)
  async fetchData(url: string): Promise<any> {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json();
  }
}

Deprecated 装饰器

function Deprecated(message?: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod = descriptor.value;

    descriptor.value = function (...args: any[]) {
      console.warn(
        `⚠️ ${propertyKey} is deprecated. ${message || "Use alternative method."}`
      );
      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

class LegacyService {
  @Deprecated("Use newMethod() instead")
  oldMethod() {
    // ...
  }
}

装饰器实现依赖注入

// 简单的依赖注入容器
const container = new Map<string, any>();

function Injectable(key: string) {
  return function (constructor: any) {
    container.set(key, new constructor());
  };
}

function Inject(key: string) {
  return function (target: any, propertyKey: string) {
    Object.defineProperty(target, propertyKey, {
      get: () => container.get(key),
      enumerable: true,
      configurable: true
    });
  };
}

@Injectable("Logger")
class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

@Injectable("UserService")
class UserService {
  @Inject("Logger")
  private logger!: Logger;

  getUser(id: number) {
    this.logger.log(`Getting user ${id}`);
    // ...
  }
}

与框架配合使用

NestJS 风格的装饰器

// 路由装饰器
function Controller(prefix: string) {
  return function (constructor: Function) {
    Reflect.defineMetadata("prefix", prefix, constructor);
  };
}

function Get(path: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    Reflect.defineMetadata("method", "GET", target, propertyKey);
    Reflect.defineMetadata("path", path, target, propertyKey);
  };
}

function Post(path: string) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    Reflect.defineMetadata("method", "POST", target, propertyKey);
    Reflect.defineMetadata("path", path, target, propertyKey);
  };
}

@Controller("/api/users")
class UserController {
  @Get("/")
  getAll() {
    return [];
  }

  @Post("/")
  create() {
    return {};
  }
}

业务场景:表单验证

// 验证装饰器
function MinLength(min: number) {
  return function (target: any, propertyKey: string) {
    let value: string;

    Object.defineProperty(target, propertyKey, {
      get: () => value,
      set: (newVal: string) => {
        if (newVal.length < min) {
          throw new Error(`${propertyKey} must be at least ${min} characters`);
        }
        value = newVal;
      },
      enumerable: true,
      configurable: true
    });
  };
}

function Email(target: any, propertyKey: string) {
  let value: string;

  Object.defineProperty(target, propertyKey, {
    get: () => value,
    set: (newVal: string) => {
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(newVal)) {
        throw new Error(`${propertyKey} must be a valid email`);
      }
      value = newVal;
    },
    enumerable: true,
    configurable: true
  });
}

class RegistrationForm {
  @MinLength(3)
  username!: string;

  @Email
  email!: string;

  @MinLength(8)
  password!: string;
}

const form = new RegistrationForm();
form.username = "alice";       // ✅
form.email = "alice@test.com"; // ✅
// form.username = "ab";       // ❌ Error: username must be at least 3 characters

注意事项

  1. 装饰器的执行顺序:工厂函数从上到下,装饰器本身从下到上
  2. 属性描述符:方法装饰器可以通过返回新的 PropertyDescriptor 来替换方法
  3. 元数据反射:使用 Reflect.metadata 需要引入 reflect-metadata
  4. 标准 vs 实验性:TypeScript 5.0+ 的标准装饰器语法与旧版实验性装饰器不同
  5. 框架选择:NestJS、Angular 等框架大量使用实验性装饰器

扩展阅读