Skip to content

装饰器与元数据(Experimental)

注意:TypeScript 装饰器目前处于实验阶段。
TypeScript 5.0 起,已开始支持 ECMAScript 装饰器提案(Stage 3),与旧版 experimentalDecorators 不兼容。
本节基于 新版标准装饰器 讲解,同时说明与 reflect-metadata 的协作方式。

核心问题解析

问题一:类装饰器怎么修改类结构?

装饰器是一种特殊函数,用于拦截和修改类、方法、属性的定义

ts
@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}

@sealed 可以在运行时冻结类构造器或原型,防止修改。

答案:通过类装饰器函数,接收类的 constructorclassDescriptor,并可返回新定义。

问题二:如何在运行时读取类型信息?

TypeScript 的类型在编译后会被擦除:

ts
class User {
  name: string;
  age: number;
}
// 编译后 JS 中没有 string 和 number 类型信息

但我们希望在运行时知道某个参数是 stringUser 类型。

答案:使用 emitDecoratorMetadata + reflect-metadata 在运行时存储和读取类型元数据。

问题三:reflect-metadata 怎么配合 TS 使用?

reflect-metadata 是一个 polyfill,提供了在对象上存储和读取元数据的 API。

结合 TS 的 emitDecoratorMetadata 选项,可以自动将类型信息写入元数据。

学习目标详解

目标一:理解实验性装饰器语法(experimentalDecorators

1. 启用装饰器

tsconfig.json 中启用:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

自 TS 5.0+,推荐使用标准装饰器(无需 experimentalDecorators),但目前生态仍广泛使用旧模式。

2. 装饰器类型

类型用法参数
类装饰器@decorator class C {}(target: Function) => void | Function
方法装饰器@decorator method() {}(target, propertyKey, descriptor)
属性装饰器@decorator prop: string;(target, propertyKey)
参数装饰器method(@decorator param: string)(target, propertyKey, parameterIndex)

目标二:实现类、方法、属性装饰器

1. 类装饰器:@logger

ts
function logger<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    timestamp = Date.now();
  };
}

@logger
class Person {
  constructor(public name: string) {}
}

const p = new Person('Alice');
// p 现在有 timestamp 属性

2. 方法装饰器:@readonly

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

class MathLib {
  @readonly
  pi() {
    return 3.14159;
  }
}

3. 属性装饰器:@observable

ts
function observable(target: any, propertyKey: string) {
  let value = target[propertyKey];

  Object.defineProperty(target, propertyKey, {
    get() { return value; },
    set(newValue) {
      console.log(`${propertyKey} changed to ${newValue}`);
      value = newValue;
    },
    enumerable: true,
    configurable: true
  });
}

class Store {
  @observable
  count = 0;
}

const s = new Store();
s.count = 1; // 控制台输出:count changed to 1

4. 参数装饰器:@inject

ts
function inject(service: string) {
  return (target: any, propertyKey: string | undefined, parameterIndex: number) => {
    console.log(`Inject ${service} into ${propertyKey} at index ${parameterIndex}`);
  };
}

class UserService {
  constructor(@inject('db') private db: any) {}
}

目标三:结合 reflect-metadata 存储类型元数据

1. 安装依赖

bash
npm install reflect-metadata

在入口文件导入:

ts
import 'reflect-metadata';

2. 启用元数据 emit

确保 tsconfig.json 包含:

json
{
  "emitDecoratorMetadata": true
}

3. 读取类型元数据

ts
class User {
  id: number;
  name: string;
  createdAt: Date;
}

// TS 会自动为构造函数参数 emit 元数据
const types = Reflect.getMetadata('design:paramtypes', User);
// types: [Number, String, Date]

const returnType = Reflect.getMetadata('design:returntype', User.prototype, 'toString');
// 如果有 toString 方法,返回其返回类型

const propType = Reflect.getMetadata('design:type', User.prototype, 'name');
// propType: String

4. 自定义元数据

ts
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey);
  console.log(`Method ${propertyKey} expects params of type:`, paramTypes);
}

class ApiService {
  @validate
  createUser(name: string, age: number) {
    // ...
  }
}
// 输出:Method createUser expects params of type: [String, Number]

目标四:了解装饰器在 NestJS 等框架中的应用

1. NestJS:依赖注入 + 控制器路由

ts
@Controller('users')
export class UsersController {
  constructor(private readonly userService: UserService) {}

  @Get()  // 方法装饰器:注册 GET 路由
  findAll(): User[] {
    return this.userService.findAll();
  }

  @Post()
  @UsePipes(ValidationPipe) // 方法装饰器:应用管道
  create(@Body() userDto: CreateUserDto): User {
    // @Body() 参数装饰器:从请求体提取数据
    return this.userService.create(userDto);
  }
}

NestJS 利用:

  • @Controller:类装饰器,标记为控制器
  • @Get@Post:方法装饰器,注册路由
  • @Body@Param:参数装饰器,提取请求数据
  • @Inject:属性/参数装饰器,实现依赖注入

2. TypeORM:实体映射

ts
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column('datetime')
  createdAt: Date;
}

装饰器将类映射到数据库表结构。

总结:本节核心要点

技术用途关键配置
类装饰器修改类定义@decorator class C
方法/属性装饰器拦截方法调用、属性访问@readonly method()
reflect-metadata运行时读取类型信息import 'reflect-metadata'
emitDecoratorMetadata自动 emit 类型元数据tsconfig.json 选项
NestJS / TypeORM装饰器驱动的框架设计依赖注入、AOP、ORM 映射

装饰器 = 元编程 + AOP(面向切面编程)
它让你能在不修改业务逻辑的前提下,添加日志、权限、验证、路由等横切关注点。

练习题

练习题 1:实现 @deprecated 装饰器

ts
function deprecated(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const originalMethod = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.warn(`Method ${propertyKey} is deprecated.`);
    return originalMethod.apply(this, args);
  };
}

class Legacy {
  @deprecated
  oldMethod() {
    return 'old';
  }
}

练习题 2:读取构造函数参数类型

ts
class PaymentService {
  constructor(
    private logger: Logger,
    private db: Database
  ) {}
}

const paramTypes = Reflect.getMetadata('design:paramtypes', PaymentService);
// [Logger, Database] ← 假设这些类已定义

练习题 3:自定义元数据装饰器

ts
function Role(role: string) {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    Reflect.defineMetadata('role', role, target, propertyKey);
  };
}

function getRole(target: any, propertyKey: string) {
  return Reflect.getMetadata('role', target, propertyKey);
}

class AdminController {
  @Role('admin')
  deleteUser() {}
}

getRole(AdminController.prototype, 'deleteUser'); // 'admin'

练习题 4:属性装饰器 + 元数据

ts
function MinLength(length: number) {
  return (target: any, propertyKey: string) => {
    Reflect.defineMetadata('minLength', length, target, propertyKey);
  };
}

class CreateUserDto {
  @MinLength(3)
  username: string;
}

Reflect.getMetadata('minLength', CreateUserDto.prototype, 'username'); // 3

练习题 5:NestJS 风格的 @Get

ts
function Get(path: string) {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    // 在框架中会注册路由
    console.log(`GET ${path} -> ${propertyKey}`);
  };
}

class UserController {
  @Get('/users')
  getAll() {}
}
// 输出:GET /users -> getAll

装饰器是 TypeScript 中最强大的元编程特性之一,尤其在构建框架时极为有用。
虽然目前仍处于实验阶段,但结合 reflect-metadata,它已经支撑起了 NestJS、TypeORM 等企业级框架的底层架构。
掌握它,你就能理解现代 TS 框架的设计哲学。