TypeScript 装饰器

308 阅读6分钟

装饰器示意图

装饰器(Decorators) 是 TypeScript 中一个强大且优雅的特性,它允许你通过声明式语法在类、方法或属性级别添加额外功能。作为 ECMAScript 提案的一部分,TypeScript 率先实现了这一特性,使其成为实现面向切面编程(AOP)的理想工具。

为什么需要装饰器?

在软件开发中,我们经常遇到一些横切关注点(Cross-Cutting Concerns)

  • 日志记录
  • 身份验证
  • 验证
  • 缓存
  • 性能监控

传统OOP方法会导致代码重复和核心逻辑的污染。装饰器通过提供一种声明式的方式来解决这些问题:

// 原始类
class UserService {
  getUser(id: number) {
    // 业务逻辑
  }
}

// 使用装饰器增强
class UserService {
  @LogExecutionTime
  @CacheResult
  getUser(id: number) {
    // 核心业务逻辑保持简洁
  }
}

启用装饰器支持

装饰器目前是一个实验性功能,需要在 tsconfig.json 中启用:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

还需安装Babel插件(若使用Babel):

npm install --save-dev @babel/plugin-proposal-decorators

装饰器类型与基本语法

TypeScript支持五种主要装饰器类型:

1. 类装饰器

应用于类构造函数,用于修改或替换类定义

function LogClass(target: Function) {
  console.log(`类装饰器应用于: ${target.name}`);
  
  // 添加元数据
  Reflect.defineMetadata('createdAt', new Date(), target);
}

@LogClass
class ApiService {
  // 类实现
}

类装饰器工厂

通过工厂函数向装饰器传递参数:

function LogClass(prefix: string) {
  return function(target: Function) {
    console.log(`${prefix}: ${target.name} 类已装饰`);
    
    // 添加类元数据
    Reflect.defineMetadata('prefix', prefix, target);
  }
}

@LogClass('API')
class ApiService {}

2. 方法装饰器

应用于类的方法,用于修改方法行为

function LogMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  
  descriptor.value = function(...args: any[]) {
    console.log(`调用方法: ${propertyKey},参数: ${JSON.stringify(args)}`);
    const result = originalMethod.apply(this, args);
    console.log(`方法返回: ${JSON.stringify(result)}`);
    return result;
  };
  
  return descriptor;
}

class UserService {
  @LogMethod
  getUser(id: number) {
    return { id, name: 'John Doe' };
  }
}

3. 访问器装饰器

应用于类属性的 getter/setter

function ReadOnly(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  descriptor.writable = false;
  return descriptor;
}

class User {
  private _name: string;
  
  constructor(name: string) {
    this._name = name;
  }
  
  @ReadOnly
  get name() {
    return this._name;
  }
}

const user = new User('Alice');
user.name = 'Bob'; // 错误: 无法分配到只读属性

4. 属性装饰器

应用于类属性

function DefaultValue(value: any) {
  return function(target: any, propertyKey: string) {
    // 设置属性默认值
    if (!target[propertyKey]) {
      target[propertyKey] = value;
    }
    
    // 添加元数据
    Reflect.defineMetadata('defaultValue', value, target, propertyKey);
  }
}

class Settings {
  @DefaultValue('light')
  theme: string;
  
  @DefaultValue(10)
  itemsPerPage: number;
}

const settings = new Settings();
console.log(settings.theme); // "light"
console.log(settings.itemsPerPage); // 10

5. 参数装饰器

应用于类方法参数

function ValidateParam(type: 'number' | 'string' | 'boolean') {
  return function(target: any, methodName: string, parameterIndex: number) {
    // 注册验证元数据
    const existingValidations = Reflect.getMetadata('validations', target, methodName) || [];
    existingValidations.push({ 
      index: parameterIndex, 
      type 
    });
    Reflect.defineMetadata('validations', existingValidations, target, methodName);
  }
}

装饰器执行顺序

理解装饰器的执行顺序至关重要:

function DecoratorA() { 
  console.log('A 工厂');
  return (target: any) => console.log('A 装饰器');
}

function DecoratorB() { 
  console.log('B 工厂');
  return (target: any) => console.log('B 装饰器');
}

@DecoratorA()
@DecoratorB()
class Example {
  @DecoratorA()
  @DecoratorB()
  method() {}
}

执行结果为:

A 工厂
B 工厂
B 装饰器
A 装饰器
A 工厂
B 工厂
B 装饰器
A 装饰器

执行规则

  1. 从上到下执行类装饰器工厂
  2. 从下到上应用类装饰器
  3. 对于每个成员:
    • 从上到下执行方法/属性装饰器工厂
    • 从下到上应用方法/属性装饰器

元数据反射 API

结合 reflect-metadata 库可以更强大地使用装饰器:

npm install reflect-metadata

在应用入口导入:

import "reflect-metadata";

使用元数据示例

// 定义类型验证装饰器
function ValidateTypes(target: any, methodName: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  const validations = Reflect.getMetadata('validations', target, methodName) || [];
  
  descriptor.value = function(...args: any[]) {
    // 参数验证
    validations.forEach(validation => {
      const paramValue = args[validation.index];
      const expectedType = validation.type;
      
      if (typeof paramValue !== expectedType) {
        throw new Error(
          `参数 #${validation.index} 应为 ${expectedType} 类型, 实际为 ${typeof paramValue}`
        );
      }
    });
    
    return originalMethod.apply(this, args);
  };
  
  return descriptor;
}

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

const calc = new Calculator();
calc.add(2, 3); // 5
calc.add(2, '3'); // 错误: 参数 #1 应为 number 类型, 实际为 string

装饰器实战应用

1. API 路由装饰器(Express/Koa)

const routes: any[] = [];

function Controller(prefix: string = '') {
  return function(target: any) {
    // 存储控制器元数据
    Reflect.defineMetadata('prefix', prefix, target);
    
    if (!Reflect.hasMetadata('routes', target)) {
      Reflect.defineMetadata('routes', [], target);
    }
  };
}

function Get(path: string) {
  return function(target: any, propertyKey: string) {
    const routes: any[] = Reflect.getMetadata('routes', target.constructor) || [];
    
    routes.push({
      method: 'get',
      path,
      handlerName: propertyKey
    });
    
    Reflect.defineMetadata('routes', routes, target.constructor);
  };
}

function Post(path: string) {
  return function(target: any, propertyKey: string) {
    // 类似Get装饰器实现...
  };
}

// 使用示例
@Controller('/users')
class UserController {
  @Get('/')
  getAllUsers() {
    return { users: [] };
  }
  
  @Get('/:id')
  getUserById(id: string) {
    return { id, name: 'Alice' };
  }
  
  @Post('/')
  createUser() {
    // 创建用户
  }
}

// 注册路由的函数
function registerControllers(app: any, controllers: any[]) {
  controllers.forEach(Controller => {
    const prefix = Reflect.getMetadata('prefix', Controller);
    const instance = new Controller();
    const routes: any[] = Reflect.getMetadata('routes', Controller) || [];
    
    routes.forEach(route => {
      app[route.method](
        `${prefix}${route.path}`, 
        instance[route.handlerName].bind(instance)
      );
      console.log(`注册路由: [${route.method.toUpperCase()}] ${prefix}${route.path}`);
    });
  });
}

// Express应用中使用(伪代码)
import express from 'express';
const app = express();
registerControllers(app, [UserController]);
app.listen(3000);

2. 依赖注入系统

const dependencies = new Map();

function Injectable(key: string) {
  return function(target: any) {
    dependencies.set(key, target);
  };
}

function Inject(key: string) {
  return function(target: any, propertyKey: string) {
    // 等待类实例化时解析依赖
    Object.defineProperty(target, propertyKey, {
      get: () => dependencies.get(key),
      enumerable: true,
      configurable: true
    });
  };
}

// 使用示例
@Injectable('UserRepository')
class UserRepository {
  getUsers() {
    return [{ id: 1, name: 'Alice' }];
  }
}

@Injectable('UserService')
class UserService {
  @Inject('UserRepository')
  userRepository!: UserRepository;
  
  getUsers() {
    return this.userRepository.getUsers();
  }
}

@Injectable('AppController')
class AppController {
  @Inject('UserService')
  userService!: UserService;
  
  run() {
    const users = this.userService.getUsers();
    console.log('Users:', users);
  }
}

// 入口点
const main = () => {
  const controller = dependencies.get('AppController');
  controller.run();
};

main(); // 输出 Users: [ { id: 1, name: 'Alice' } ]

3. ORM 实体模型

import "reflect-metadata";

const ENTITY_METADATA_KEY = Symbol('entity:fields');

function Entity(tableName: string) {
  return function(target: any) {
    Reflect.defineMetadata('table', tableName, target);
  };
}

function Column(options: { type: string, primary?: boolean, nullable?: boolean }) {
  return function(target: any, propertyKey: string) {
    const fields = Reflect.getMetadata(ENTITY_METADATA_KEY, target) || [];
    
    fields.push({
      name: propertyKey,
      type: options.type,
      primary: options.primary || false,
      nullable: options.nullable || false
    });
    
    Reflect.defineMetadata(ENTITY_METADATA_KEY, fields, target);
  };
}

// 使用示例
@Entity('users')
class User {
  @Column({ type: 'number', primary: true })
  id: number;
  
  @Column({ type: 'string' })
  name: string;
  
  @Column({ type: 'string', nullable: true })
  email?: string;
  
  constructor(id: number, name: string, email?: string) {
    this.id = id;
    this.name = name;
    this.email = email;
  }
}

// ORM工具函数
function save(entity: any) {
  const table = Reflect.getMetadata('table', entity.constructor);
  const fields = Reflect.getMetadata(ENTITY_METADATA_KEY, entity) || [];
  
  const columns = [];
  const values = [];
  
  fields.forEach(field => {
    const value = entity[field.name];
    
    if (value !== undefined || field.nullable) {
      columns.push(field.name);
      values.push(value);
    } else if (!field.nullable) {
      throw new Error(`字段 ${field.name} 不能为空`);
    }
  });
  
  const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${columns.map((_, i) => '?').join(', ')})`;
  console.log('执行SQL:', sql);
  console.log('参数:', values);
  
  // 实际执行数据库操作...
}

// 使用ORM
const user = new User(1, 'Alice', 'alice@example.com');
save(user);

4. 响应式属性装饰器(Vue/Angular风格)

function ReactiveProperty(target: any, propertyKey: string) {
  const privateKey = `_${propertyKey}`;
  
  // 替换属性定义
  Object.defineProperty(target, propertyKey, {
    get: function() {
      return this[privateKey];
    },
    set: function(newValue) {
      if (this[privateKey] !== newValue) {
        const oldValue = this[privateKey];
        this[privateKey] = newValue;
        
        // 通知变化
        if (this.propertyChanged) {
          this.propertyChanged(propertyKey, oldValue, newValue);
        }
      }
    },
    enumerable: true,
    configurable: true
  });
}

// 使用示例
class ViewModel {
  // 变化通知事件
  propertyChanged?: (property: string, oldValue: any, newValue: any) => void;
  
  @ReactiveProperty
  firstName = 'John';
  
  @ReactiveProperty
  lastName = 'Doe';
  
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
  
  constructor() {
    // 监听属性变化
    this.propertyChanged = (prop, oldVal, newVal) => {
      console.log(`属性 "${prop}" 已更改: ${oldVal} -> ${newVal}`);
      if (prop === 'firstName' || prop === 'lastName') {
        console.log('全名更新为:', this.fullName);
      }
    };
  }
}

const vm = new ViewModel();
vm.firstName = 'Alice'; 
// 输出: 
// 属性 "firstName" 已更改: John -> Alice
// 全名更新为: Alice Doe

vm.lastName = 'Smith';
// 输出:
// 属性 "lastName" 已更改: Doe -> Smith
// 全名更新为: Alice Smith

装饰器最佳实践

1. 保持装饰器职责单一

不良实践:

function LogAndValidate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // 同时处理日志和验证 - 违反单一职责原则
}

良好实践:

@LogExecutionTime
@ValidateParameters
@CacheResult(60)
async getUser(id: number) {
  // ...
}

2. 避免副作用,保持幂等性

装饰器应该是幂等的:

// 良好实践:幂等装饰器
function Decorate(target: any) {
  if (!target.__decorated) {
    // 应用装饰逻辑
    target.__decorated = true;
  }
}

3. 提供清晰的自定义选项

使用配置对象而不是多个参数:

// 更易读的配置
function Cache(options: { ttl: number; key?: string; }) {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    // 实现
  };
}

4. 考虑继承和装饰器链

正确处理装饰器在继承链中的行为:

class BaseClass {
  @DecoratorA
  method() {}
}

class ChildClass extends BaseClass {
  @DecoratorB
  method() {}
}

// DecoratorA 和 DecoratorB 都会应用到 ChildClass 的 method

5. 谨慎使用元数据反射

避免过度使用反射以减少性能开销:

// 在可能的情况下缓存反射结果
const cachedMetadata = new Map();

function getMetadata(target: any) {
  if (!cachedMetadata.has(target)) {
    cachedMetadata.set(target, Reflect.getMetadata(...));
  }
  return cachedMetadata.get(target);
}

TypeScript 装饰器的未来

虽然装饰器目前处于Stage 2提案阶段,但TypeScript团队正积极与TC39合作推进标准。当前实现与未来标准的主要差异:

特性当前TS实现ECMAScript提案
语法@decorator相同
类装饰器目标构造函数构造函数
参数顺序目标、键、描述符值、上下文对象
上下文信息有限的提供完整上下文对象

迁移策略:

// 当前TypeScript风格
function legacyDecorator(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  // ...
}

// 未来兼容风格
function standardDecorator(value: any, context: DecoratorContext) {
  if (context.kind === 'method') {
    // 处理类方法
  }
}

元编程的艺术

装饰器为TypeScript开发提供了强大的元编程能力,使开发者能够:

  • 🧩 优雅地实现横切关注点分离
  • 🚀 实现声明式编程范式
  • 🔗 构建复杂框架(Angular、NestJS)
  • 📦 创建可重用功能组件
  • ⚙️ 增强元编程能力(依赖注入、AOP)

关键知识点回顾:

  1. 五种装饰器类型:类、方法、访问器、属性、参数
  2. 装饰器工厂模式实现参数化
  3. 反射元数据API的集成使用
  4. 装饰器执行顺序规则
  5. 真实世界应用:Web框架、ORM、DI、响应式编程

正如设计模式大师Robert C. Martin所说:"装饰器模式允许向对象动态添加行为而不改变对象本身。" TypeScript装饰器将这一理念提升到语言级别,为现代应用开发提供了强大的抽象能力。

掌握装饰器,你将能够:

  • 开发企业级框架和库
  • 编写更清晰、更模块化的代码
  • 实现复杂的横切关注点处理
  • 构建声明式API系统
  • 提升架构设计能力