最近在学习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' };
}
}
问题:
UserService
和Logger
强耦合。- 想换一个别的 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.getMetadata
是reflect-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
拿到。