简单理解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 拿到。

相关推荐
augenstern4161 小时前
HTML面试题
前端·html
张可1 小时前
一个KMP/CMP项目的组织结构和集成方式
android·前端·kotlin
G等你下课1 小时前
React 路由懒加载入门:提升首屏性能的第一步
前端·react.js·前端框架
蓝婷儿2 小时前
每天一个前端小知识 Day 27 - WebGL / WebGPU 数据可视化引擎设计与实践
前端·信息可视化·webgl
然我2 小时前
面试官:如何判断元素是否出现过?我:三种哈希方法任你选
前端·javascript·算法
OpenTiny社区3 小时前
告别代码焦虑,单元测试让你代码自信力一路飙升!
前端·github
pe7er3 小时前
HTTPS:本地开发绕不开的设置指南
前端
晨枫阳3 小时前
前端VUE项目-day1
前端·javascript·vue.js
江山如画,佳人北望3 小时前
SLAM 前端
前端
患得患失9493 小时前
【前端】【Iconify图标库】【vben3】createIconifyIcon 实现图标组件的自动封装
前端