Chapter 09

装饰器与元编程

掌握 TypeScript Stage 3 装饰器,了解 reflect-metadata 与 NestJS/TypeORM 的实际应用

装饰器(Decorators)

什么是装饰器?

装饰器是一种特殊的声明,可以附加到类、方法、访问器、属性或参数上,用于修改或增强它们的行为。它的本质是一个函数,在类定义被求值时被调用,接收目标元素作为参数,可以选择返回修改后的版本或读取/写入元数据。

装饰器实现了 AOP(面向切面编程)模式:将日志记录、权限检查、参数验证等横切关注点(cross-cutting concerns)从业务逻辑中分离出来,以声明式的方式附加到代码上,而不需要修改业务代码本身。

装饰器的两个时代

TypeScript 有两套装饰器实现,选择哪套取决于你用的框架:

旧版装饰器(experimentalDecorators: true)
基于 TC39 Stage 1 提案(约 2016 年)。NestJS、TypeORM、Angular 等框架广泛使用,生态成熟。需要同时开启 experimentalDecorators 和 emitDecoratorMetadata(用于 reflect-metadata)。与 ECMAScript 最终标准不兼容,未来会逐步被新版取代。
新版装饰器(TypeScript 5.0+,Stage 3)
基于 TC39 Stage 3 提案,是 ECMAScript 的未来标准。TypeScript 5.0 起默认支持,无需配置。API 与旧版完全不同(Context 对象替代了直接参数),不支持 reflect-metadata(元数据 API 有独立 Stage 3 提案)。当前主流框架尚未迁移,新项目建议观望。

新版装饰器(Stage 3,TS 5.0+)

// 类装饰器
function sealed(target: Function, ctx: ClassDecoratorContext) {
  Object.seal(target);
  Object.seal(target.prototype);
  console.log(`Sealed class: ${String(ctx.name)}`);
}

@sealed
class BugReport {
  type = 'report';
  title: string;
  constructor(t: string) { this.title = t; }
}

// 方法装饰器
function logCall(target: Function, ctx: ClassMethodDecoratorContext) {
  return function (this: unknown, ...args: unknown[]) {
    console.log(`调用方法: ${String(ctx.name)}, 参数:`, args);
    return target.apply(this, args);
  };
}

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

旧版装饰器与 NestJS

// tsconfig.json(NestJS 项目)
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
// NestJS 控制器示例(旧版装饰器)
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id);
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}

装饰器工厂模式

// 装饰器工厂:返回装饰器的函数,允许传参
function MinLength(min: number) {
  return function (target: unknown, ctx: ClassFieldDecoratorContext) {
    return function (this: unknown, initialValue: string) {
      if (initialValue.length < min) {
        throw new Error(`字段 ${String(ctx.name)} 最少 ${min} 个字符`);
      }
      return initialValue;
    };
  };
}

class User {
  @MinLength(3)
  name: string;

  constructor(name: string) {
    this.name = name;  // 会触发装饰器验证
  }
}

new User('张三');  // ✅
new User('张');    // ❌ Error: 字段 name 最少 3 个字符

reflect-metadata 与元数据

旧版装饰器配合 reflect-metadata 库,可以在运行时读写附加在类/方法/属性上的元数据。emitDecoratorMetadata: true 让 TypeScript 在编译时自动将类型信息(参数类型、返回类型)作为元数据写入,这正是 NestJS 依赖注入的工作原理。

import 'reflect-metadata';  // 必须最先引入

// 自定义元数据键(避免命名冲突)
const ROLES_KEY = 'roles';

// 旧版装饰器:权限控制
function Roles(...roles: string[]) {
  // 将 roles 数组作为元数据附加到方法上
  return Reflect.metadata(ROLES_KEY, roles);
}

class AppController {
  @Roles('admin')
  deleteUser(id: number) { /* ... */ }

  @Roles('user', 'admin')
  getProfile() { /* ... */ }
}

// 运行时读取元数据(用于权限检查中间件)
function checkAccess(controller: object, methodName: string, userRole: string) {
  const requiredRoles: string[] = Reflect.getMetadata(ROLES_KEY, controller, methodName) ?? [];
  return requiredRoles.length === 0 || requiredRoles.includes(userRole);
}

const ctrl = new AppController();
console.log(checkAccess(ctrl, 'deleteUser', 'user'));   // false
console.log(checkAccess(ctrl, 'getProfile', 'user'));   // true

新版装饰器的五种类型

ClassDecoratorContext
类装饰器的上下文对象,包含 kind: 'class'、name(类名)、addInitializer()(添加初始化器)。类装饰器在类定义被求值后立即执行,可以返回一个新的类替代原类(类似 Proxy),也可以只读取/修改 prototype。
ClassMethodDecoratorContext
方法装饰器的上下文,包含 kind: 'method'、name、static(是否静态)、private(是否私有)、access(读写访问器)。返回的函数会替换原方法。
ClassFieldDecoratorContext
字段装饰器的上下文,包含 kind: 'field'、name、static、private。返回一个初始化函数(接受初始值,返回新初始值),在实例创建时调用。用于字段级别的验证和转换。
ClassAccessorDecoratorContext
访问器装饰器(auto accessor)的上下文,包含 kind: 'accessor'。可以重写 get 和 set 行为,是新版 Stage 3 装饰器对旧版属性装饰器的替代方案。
ClassGetterDecoratorContext / ClassSetterDecoratorContext
getter/setter 装饰器的上下文,kind 分别是 'getter' 和 'setter'。可以拦截属性的读写,实现惰性初始化、验证、日志等逻辑。
// 新版装饰器完整示例:带记忆化(memoize)的方法装饰器
function memoize<This, Args extends unknown[], Return>(
  target: (this: This, ...args: Args) => Return,
  ctx: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
): (this: This, ...args: Args) => Return {
  // 在每个实例上维护独立的缓存(WeakMap 防止内存泄漏)
  const cache = new WeakMap<object, Map<string, Return>>();

  return function (this: This, ...args: Args): Return {
    // 用 this 作为实例键(每个实例有自己的缓存)
    if (!cache.has(this as object)) {
      cache.set(this as object, new Map());
    }
    const instanceCache = cache.get(this as object)!;

    // 用序列化的参数作为缓存键
    const key = JSON.stringify(args);
    if (instanceCache.has(key)) {
      console.log(`[${String(ctx.name)}] 命中缓存`);
      return instanceCache.get(key)!;
    }

    // 未命中:执行原始方法并缓存结果
    const result = target.call(this, ...args);
    instanceCache.set(key, result);
    return result;
  };
}

class MathService {
  @memoize
  fibonacci(n: number): number {
    if (n <= 1) return n;
    // 注意:递归调用也会走缓存
    return this.fibonacci(n - 1) + this.fibonacci(n - 2);
  }
}

const svc = new MathService();
console.log(svc.fibonacci(40));  // 快速!后续相同参数直接命中缓存

TypeORM 装饰器示例

// TypeORM 实体:旧版装饰器将类映射为数据库表
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne } from 'typeorm';

@Entity()  // 声明这个类对应数据库中的一张表
export class Post {
  @PrimaryGeneratedColumn()  // 自增主键
  id: number;

  @Column({ length: 200 })    // varchar(200) 列
  title: string;

  @Column('text')             // text 类型列
  content: string;

  @Column({ default: false })  // 有默认值的列
  published: boolean;

  // ManyToOne:多篇文章属于同一作者
  @ManyToOne(() => User, user => user.posts)
  author: User;
}
生产项目建议

如果你的项目使用 NestJS 或 TypeORM,必须使用旧版装饰器(experimentalDecorators: true + emitDecoratorMetadata: true),这些框架还未迁移到新版标准。如果你在写新的独立代码,推荐等待主流框架完成迁移后再使用新版装饰器,两套 API 不互相兼容。

本章小结

本章核心要点