简单理解Typescript 装饰器

最近在学习nestjs,先简单补充一波ts装饰器的知识

Typescript 装饰器

装饰器其实就是一个函数,能接收目标的信息(如类本身、方法名、属性描述符等),然后进行操作。可以:

  • 读取元信息(类型等)
  • 修改或扩展行为
  • 返回新的构造器 / 方法 / 属性描述符

使用前提

tsconfig.json 中必须启用:

json 复制代码
{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

分类与示例

1️⃣ 类装饰器

装饰类本身(构造函数)

📌 基本示例

jsx 复制代码
function Logger(constructor: Function) {
    console.log(`类创建了 ${constructor.name}`)
}

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

// 输出
[LOG]: "类创建了 Person"当 `Person` 类被**定义**时,会自动执行 `Logger` 函数。

原理:类装饰器函数在类定义时执行,接收类的构造函数作为参数。

📌 通过 reflect-metadata 获取元信息

需要先安装并引入:

npm install reflect-metadata

import 'reflect-metadata';

然后在 tsconfig.json 开启:

jsx 复制代码
{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}

TypeScript 编译器会自动生成:

  • design:paramtypes → 构造函数参数类型数组
  • design:type → 属性的类型
  • design:returntype → 方法返回值类型

📌 修改 / 扩展类

添加方法

  • 添加静态方法/属性 (直接挂载到 constructor 上)
  • 修改原型链constructor.prototype
jsx 复制代码
function AddGreetMethod(constructor: Function) {
  constructor.prototype.greet = function () {
    console.log(`Hello ${this.name}!`)
  }
}

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

const person: any = new Person('danyang')
;(person as any).greet()

替换类实现

jsx 复制代码
function WrapClass<T extends { new (...args: any[]): {} }>(constructor: T) {
  return class extends constructor {
    constructor(...args: any[]) {
      super(...args)
      console.log(`实例已创建,参数: ${args}`)
    }
    // 可以新增方法或属性
    extraMethod() {
      console.log('这是新增的方法!')
    }
  }
}

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

const person = new Person('danyang')
;(person as any).extraMethod()

单例模式

jsx 复制代码
function Singleton<T extends { new (...args: any[]): {} }>(constructor: T) {
  let instance: T;
  return class {
    constructor(...args: any[]) {
      if (!instance) {
        instance = new constructor(...args);
      }
      return instance;
    }
  } as unknown as T;
}

@Singleton
class AppConfig {
  constructor(public env: string) {}
}

const config1 = new AppConfig('dev');
const config2 = new AppConfig('prod');
console.log(config1 === config2); // true(始终返回同一个实例)

实现依赖注入(DI)或控制反转(IoC)
💡

依赖注入 就是把一个对象所依赖的对象(=「依赖」)在外部 创建好,然后注入给它,而不是让对象自己去创建这些依赖。

简单来说:

  • 不用 new 自己去创建依赖
  • 而是让别人(容器、框架)帮你创建好并提供
jsx 复制代码
// 核心思路:获取构造函数参数类型 → 容器递归创建依赖
import 'reflect-metadata'

class Container {
  private static instances = new Map()

  static resolve<T>(target: new (...args: any[]) => T): T {
    const tokens = Reflect.getMetadata('design:paramtypes', target) || []
    const injections = tokens.map((token: any) => Container.resolve<any>(token))

    return new target(...injections)
  }
}

// 可注入装饰器(此示例中只起标记作用)
function Injectable() {
  return (target: any) => {}
}

// 可被注入的服务
@Injectable()
class Logger {
  log(message: string) {
    console.log('Logger:', message)
  }
}

// 使用依赖注入的类
@Injectable()
class UserService {
  constructor(private logger: Logger) {}

  getUser() {
    this.logger.log('正在获取用户...')

    return { id: 1, name: 'danyang' }
  }
}

const userService = Container.resolve(UserService)
userService.getUser()

举个简单例子

不使用 DI:

jsx 复制代码
class UserService {
  private logger: Logger;

  constructor() {
    this.logger = new Logger();  // 写死了
  }

  getUser() {
    this.logger.log('getUser called');
    return { id: 1, name: 'Alice' };
  }
}

问题:

  • UserServiceLogger 强耦合。
  • 想换一个别的 Logger 实现很麻烦。

使用 DI:

jsx 复制代码
class UserService {
  constructor(private logger: Logger) {}

  getUser() {
    this.logger.log('getUser called');
    return { id: 1, name: 'Alice' };
  }
}

然后在外面:

jsx 复制代码
const logger = new Logger();
const userService = new UserService(logger);

现在:

  • UserService 不需要知道 Logger 是怎么创建的
  • 只要能拿到一个符合接口/类型的 logger 就能用

🧰 再进一步:使用 DI 容器

容器帮你自动创建依赖:

const userService = Container.resolve(UserService);

  • 容器先创建 Logger 实例
  • 再注入给 UserService
  • 如果以后想换成 FileLogger,只需在容器里配置,不需要改业务类

2️⃣ 方法装饰器

装饰类方法。

记录调用日志:

jsx 复制代码
function Log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`调用 ${propertyKey}:`, args);
    return original.apply(this, args);
  };
}

class MathTool {
  @Log
  add(a: number, b: number) { return a + b; }
}
new MathTool().add(1, 2); // 调用 add: [1, 2]

设置可枚举性:

jsx 复制代码
function enumerable(value: boolean) {
  return (target: any, key: string, desc: PropertyDescriptor) => {
    desc.enumerable = value;
  };
}

class Greeter {
  @enumerable(false)
  greet() { return 'Hello'; }
}

3️⃣ 属性装饰器

装饰属性。

打印属性名:

jsx 复制代码
function LogProperty(target: any, key: string) {
  console.log(`属性定义: ${key}`);
}

class Example {
  @LogProperty
  name: string = 'TS';
}

格式化属性:

jsx 复制代码
function format(prefix: string) {
  return (target: any, key: string) => {
    let val = target[key];
    Object.defineProperty(target, key, {
      get: () => `${prefix} ${val}`,
      set: v => val = v,
      enumerable: true,
    });
  };
}

class Greeter {
  @format('Hello')
  greeting: string;
  constructor(message: string) { this.greeting = message; }
}
console.log(new Greeter('World').greeting); // Hello World

4️⃣ 参数装饰器

装饰方法参数。

jsx 复制代码
import 'reflect-metadata';

function validate(target: any, key: string, index: number) {
  const types = Reflect.getMetadata('design:paramtypes', target, key);
  console.log(`方法 ${key} 参数${index} 类型:`, types[index].name);
}

class Greeter {
  greet(@validate name: string) { return `Hello ${name}`; }
}

5️⃣ 访问器(Accessor)装饰器

访问器装饰器用于装饰类中的 getter / setter,常用于:

  • 控制可枚举性
  • 修改访问器逻辑
  • 添加日志等
jsx 复制代码
function configurable(value: boolean) {
  return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
    descriptor.configurable = value;
  };
}

class Person {
  private _name: string;

  constructor(name: string) {
    this._name = name;
  }

  @configurable(false)
  get name() {
    return this._name;
  }
}

这个例子中:

  • configurable(false) 设置 name 访问器不可重新定义(比如不能再用 Object.defineProperty 修改)

总结:

常用场景

  • 日志 / 调试
  • 权限校验
  • 自动绑定 this
  • AOP(面向切面)
  • ORM / 映射数据库(如 TypeORM)
  • 依赖注入(NestJS)
  • 单例 / 缓存

装饰器汇总

类型 接收参数 常用场景
类装饰器 constructor 日志、依赖注入、单例
方法装饰器 target, key, descriptor 日志、权限、AOP
访问器装饰器 target, key, descriptor 日志、修改 getter/setter 行为
属性装饰器 target, key ORM 映射、格式化
参数装饰器 target, key, index 校验、注入

获取不同形式的元数据

jsx 复制代码
import 'reflect-metadata'

@Logger
class Person {
  @Logger2
  myProp: number
  constructor(public name: string) {}

  @Logger3
  myMethod(@Logger4 param: boolean): boolean {
    return true
  }
}

/**
 * @param constructor 被装饰的类的构造函数
 */
function Logger(constructor: Function) {
  console.log(`类创建了 ${constructor.name}`)

  const metadata = Reflect.getMetadata('design:paramtypes', constructor)
  console.log('构造函数参数类型:', metadata)
}

/**
 *
 * @param target 对于实例属性:是 类的原型(prototype);对于静态属性:是 类的构造函数
 * @param propertyKey
 */
function Logger2(target: any, propertyKey: string) {
  const propType = Reflect.getMetadata('design:type', target, propertyKey)
  console.log('属性类型', propType)
}

/**
 * @param target 对于实例方法,是 Example.prototype
 * @param propertyKey 方法名 'myMethod'
 * @param descriptor 方法的属性描述符
 */
function Logger3(
  target: any,
  propertyKey: string,
  descriptor: PropertyDescriptor
) {
  const returnType = Reflect.getMetadata(
    'design:returntype',
    target,
    propertyKey
  )

  console.log(`方法 "${propertyKey}" 的返回值类型是: ${returnType?.name}`)
}

/**
 *
 * @param target 同样是类的原型或构造函数
 * @param propertyKey 方法名
 * @param parameterIndex 参数索引
 */
function Logger4(
  target: Object,
  propertyKey: string | symbol,
  parameterIndex: number
) {
  console.log(`装饰器 Logger4 执行:`)
  console.log(`target:`, target)
  console.log(`方法名:`, propertyKey)
  console.log(`参数索引:`, parameterIndex)

  // 获取参数类型数组
  const paramTypes = Reflect.getMetadata(
    'design:paramtypes',
    target,
    propertyKey
  )
  console.log(
    `参数类型数组:`,
    paramTypes.map((type: any) => type.name)
  )

  // 单独拿到被装饰参数的类型
  const thisParamType = paramTypes[parameterIndex]
  console.log(`被装饰参数的类型:`, thisParamType.name)
}

构造函数constructor)上读取由 TypeScript 装饰器编译器生成的元数据

具体来说:

  • Reflect.getMetadatareflect-metadata 提供的 API,用来读取元数据
  • 'design:paramtypes' 是一个固定的元数据键(metadata key),由 TypeScript 编译器自动生成,用来表示构造函数的参数类型列表。
  • constructor 是目标类(或函数)的构造函数。

最终返回的 metadata 通常是一个数组,包含构造函数参数的类型信息: [ String, Number, SomeClass, ... ]

元数据是什么 当你在 tsconfig.json 中开启:

jsx 复制代码
{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}

并且给类或类的构造函数、属性等加上装饰器时,TypeScript 编译器就会自动在目标对象上添加一些元数据,比如:

  • 'design:paramtypes' → 构造函数参数类型数组
  • 'design:type' → 属性的类型
  • 'design:returntype' → 方法的返回类型

这些信息就可以在运行时通过 Reflect.getMetadata 拿到。

相关推荐
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端
爱敲代码的小鱼13 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax