Skip to content

装饰器与元数据

一、TypeScript 装饰器本质

装饰器是一个在类/方法/属性/参数被定义时执行的函数(注意:是类定义时,不是实例化时,也不是调用时)。

TypeScript 目前支持两套装饰器:

  • Legacy DecoratorsexperimentalDecorators: true)— NestJS 使用此版本
  • Stage 3 Decorators(TC39 提案,TS 5.0+ 支持)— 与 NestJS 不兼容
typescript
// tsconfig.json 中必须开启:
{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

四种装饰器类型

typescript
// 1. 类装饰器:接收构造函数,返回值可替换原类
function Injectable(): ClassDecorator {
  return (target: Function) => {
    Reflect.defineMetadata('injectable', true, target);
  };
}

// 2. 方法装饰器:接收原型、方法名、属性描述符
function Get(path: string): MethodDecorator {
  return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => {
    Reflect.defineMetadata('path', path, target, key as string);
    Reflect.defineMetadata('method', 'GET', target, key as string);
  };
}

// 3. 属性装饰器:接收原型、属性名
function Column(): PropertyDecorator {
  return (target: object, key: string | symbol) => {
    const columns = Reflect.getMetadata('columns', target) || [];
    Reflect.defineMetadata('columns', [...columns, key], target);
  };
}

// 4. 参数装饰器:接收原型、方法名、参数索引
function Body(): ParameterDecorator {
  return (target: object, key: string | symbol, index: number) => {
    const params = Reflect.getMetadata('body_params', target, key as string) || [];
    params[index] = 'body';
    Reflect.defineMetadata('body_params', params, target, key as string);
  };
}

装饰器执行顺序

多个装饰器叠加时,执行顺序是从下到上(求值是从上到下,执行是从下到上):

typescript
@A        // 第2步执行
@B        // 第1步执行
@C        // 先执行(最接近被装饰目标)
class Foo {}

// 等价于:A(B(C(Foo)))

类上的多个方法装饰器,按方法定义顺序执行:

typescript
@Controller('users')
export class UsersController {
  @Get()            // 先处理
  findAll() {}

  @Get(':id')       // 后处理
  findOne() {}
}

二、reflect-metadata 深度解析

reflect-metadata 是 TC39 提案的 polyfill,提供了一套标准化的元数据 API。

核心 API:

typescript
// 定义元数据
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey); // 方法/属性上

// 读取元数据(含继承链)
Reflect.getMetadata(metadataKey, target);
Reflect.getMetadata(metadataKey, target, propertyKey);

// 读取元数据(仅自身,不含继承)
Reflect.getOwnMetadata(metadataKey, target);

// 检查是否存在
Reflect.hasMetadata(metadataKey, target);

// 获取所有元数据 key
Reflect.getMetadataKeys(target);

// 删除
Reflect.deleteMetadata(metadataKey, target);

emitDecoratorMetadata 的魔法

emitDecoratorMetadata: true 时,TypeScript 编译器会在有装饰器的地方自动注入三个元数据:

元数据键说明
design:type属性的类型属性装饰器可用
design:paramtypes构造函数参数类型数组类装饰器可用
design:returntype方法返回类型方法装饰器可用
typescript
@Injectable()
class UserService {
  constructor(
    private repo: UserRepository,  // TypeScript 类型
    private config: ConfigService,
  ) {}
}

// 编译后,TypeScript 自动注入:
// Reflect.defineMetadata('design:paramtypes',
//   [UserRepository, ConfigService],
//   UserService
// )

// NestJS DI 容器读取:
const types = Reflect.getMetadata('design:paramtypes', UserService);
// → [UserRepository, ConfigService]
// 然后去容器里查找对应实例并注入

注意:接口在运行时会被擦除!

typescript
// ❌ 接口在编译后消失,无法作为注入 Token
interface IUserRepo {}
constructor(private repo: IUserRepo) {}  // design:paramtypes 中是 Object

// ✅ 使用抽象类或具体类
abstract class UserRepo {}
constructor(private repo: UserRepo) {}   // design:paramtypes 中是 UserRepo

三、NestJS 内置元数据键

元数据键用途由谁设置
design:paramtypes构造函数参数类型TypeScript 自动注入
path路由路径@Controller()/@Get()
methodHTTP 方法@Get()/@Post()
guards守卫列表@UseGuards()
interceptors拦截器列表@UseInterceptors()
pipes管道列表@UsePipes()
filters异常过滤器@UseFilters()
roles角色要求自定义 @Roles()
__isController__标记是否是 Controller@Controller()
__isRouteParamMetadata__路由参数绑定@Param()/@Body()

四、自定义元数据实战

场景一:角色权限

typescript
// 定义装饰器(推荐用 SetMetadata 工厂函数)
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// 使用
@Get('admin')
@Roles('admin', 'superuser')
getAdminData() {}

// 在守卫中读取
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // getAllAndOverride:方法级优先,回退到类级
    const roles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),  // 方法上的 @Roles
      context.getClass(),    // 类上的 @Roles
    ]);
    if (!roles) return true;  // 无角色限制,放行
    const user = context.switchToHttp().getRequest().user;
    return roles.some(role => user.roles.includes(role));
  }
}

场景二:接口版本标记

typescript
export const ApiVersion = (version: number) =>
  SetMetadata('api_version', version);

@Get('users')
@ApiVersion(2)
findAllV2() {}

场景三:公开接口(跳过认证)

typescript
export const Public = () => SetMetadata('isPublic', true);

@Public()
@Get('health')
healthCheck() { return 'ok'; }

// JWT 守卫中判断
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) { super(); }

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (isPublic) return true;  // 直接放行
    return super.canActivate(context);
  }
}

五、Reflector 的三种方法对比

Reflector 是 NestJS 对 Reflect.getMetadata 的封装,提供更便捷的 API:

typescript
// get:只读取方法/类上的元数据(不合并)
const roles = reflector.get<string[]>('roles', context.getHandler());

// getAllAndMerge:收集类和方法上的所有值,合并为一个数组
const allRoles = reflector.getAllAndMerge<string[]>('roles', [
  context.getHandler(),
  context.getClass(),
]);
// 若类上是 ['admin'],方法上是 ['editor'],结果是 ['admin', 'editor']

// getAllAndOverride:方法级优先,若方法上没有则用类上的
const effectiveRoles = reflector.getAllAndOverride<string[]>('roles', [
  context.getHandler(),
  context.getClass(),
]);
// 若方法上是 ['editor'],直接返回 ['editor'](不管类上是什么)

使用原则:

  • getAllAndOverride:权限控制(方法级覆盖类级)
  • getAllAndMerge:标签/分组(累积收集)
  • get:简单场景,只关心一个层级

六、常见陷阱

陷阱 1:忘记在 tsconfig.json 开启选项

json
// 缺少这两项,DI 和很多装饰器功能会失效
{
  "experimentalDecorators": true,   // ← 必须
  "emitDecoratorMetadata": true     // ← 必须
}

陷阱 2:装饰器在 Class 外使用

typescript
// ❌ 装饰器只能用于类、方法、属性、参数
@Injectable()
const fn = () => {};  // 语法错误

陷阱 3:继承时元数据不自动继承

typescript
@Controller('base')
class BaseController {
  @Get()
  findAll() {}
}

// 子类不会继承父类的 @Get() 装饰器行为
// NestJS 不支持通过继承复用路由——使用 Mixin 模式替代
class UsersController extends BaseController {}

可运行 Demo: practice/01-ioc-demo — 观察 reflect-metadata 元数据读写(npm test


常见错误

错误原因解决
Reflect.getMetadata 返回 undefinedemitDecoratorMetadata 未开启tsconfig.json 设置 "emitDecoratorMetadata": true
装饰器执行顺序出错多个装饰器从下到上执行记住:@A @B 等价于 A(B(target)),B 先执行
参数装饰器拿不到类型TypeScript 编译目标太低"target": "ES2020" 以上才能正确发射元数据

NestJS 深度学习体系