Skip to content

第11章 装饰器与元编程

本章定位:深入学习TypeScript的装饰器(Decorator)机制,包括类装饰器、方法装饰器、属性装饰器、参数装饰器,以及装饰器工厂和反射元数据的概念。


装饰器是一种设计模式,允许在不修改原类的情况下,给类或对象动态添加功能。

// 基础类
class Coffee {
cost(): number {
return 2;
}
}
// 装饰器
function withMilk(coffee: Coffee): Coffee {
return {
cost: () => coffee.cost() + 0.5
};
}
// 使用
const myCoffee = new Coffee();
const milkCoffee = withMilk(myCoffee);

TypeScript装饰器提供标准语法:

// 装饰器函数
function readonly(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.writable = false;
}
// 使用
class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
@readonly
distance(): number {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}

在tsconfig.json中启用:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

类装饰器接收构造函数作为参数:

// 记录所有创建的对象
function logged(target: Function) {
const original = target;
function Constructor(...args: any[]) {
console.log(`Creating instance of ${original.name}`);
return new original(...args);
}
Constructor.prototype = original.prototype;
return Constructor;
}
@logged
class User {
name: string;
constructor(name: string) {
this.name = name;
}
}
const user = new User("Alice");
// "Creating instance of User"
// 冻结类(禁止添加新属性)
function sealed(target: Function) {
Object.seal(target);
Object.seal(target.prototype);
}
// 添加属性
function withVersion(target: Function) {
target.VERSION = "1.0.0";
target.getVersion = () => target.VERSION;
}
@sealed
@withVersion
class Config {
apiUrl: string;
}
console.log(Config.VERSION); // "1.0.0"
Config.NEW_PROP = "value"; // 编译或运行时可能报错(取决于实现)
// 装饰器工厂
function prefix(prefix: string) {
return function(target: Function) {
target.prototype.prefix = prefix;
};
}
@prefix("USER_")
class User {
id: string;
constructor() {
this.id = `${(this as any).prefix}001`;
}
}
const user = new User();
console.log(user.id); // "USER_001"
function first(target: Function) {
console.log("first");
}
function second(target: Function) {
console.log("second");
}
@first
@second
class MyClass {}
// 输出:second first(由下到上)

方法装饰器接收方法描述符:

function readonly(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
descriptor.writable = false;
return descriptor;
}
class Calculator {
@readonly
PI = 3.14159;
@readonly
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.PI = 3; // 运行时可能不报错,但不应该设置
calc.add = () => 0; // 错误!方法不能修改
function validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.set;
descriptor.set = function(value: number) {
if (value < 0) {
throw new Error(`${propertyKey} must be non-negative`);
}
original?.call(this, value);
};
}
class BankAccount {
private _balance = 0;
@validate
set balance(value: number) {
this._balance = value;
}
get balance(): number {
return this._balance;
}
}
const account = new BankAccount();
account.balance = 100; // OK
account.balance = -50; // Error: balance must be non-negative
function defaultValue(value: any) {
return function(target: any, propertyKey: string) {
target[propertyKey] = value;
};
}
function capitalized(
target: any,
propertyKey: string
) {
const original = target[propertyKey];
Object.defineProperty(target, propertyKey, {
get: () => original?.toUpperCase(),
set: (v) => { original = v; },
enumerable: true,
configurable: true
});
}
class User {
@defaultValue("Anonymous")
name: string;
@capitalized
city: string;
}
const user = new User();
console.log(user.name); // "Anonymous"
user.city = "beijing";
console.log(user.city); // "BEIJING"
function staticDefault(value: any) {
return function(target: any, propertyKey: string) {
target[propertyKey] = value;
};
}
class Config {
@staticDefault("http://localhost:3000")
static API_URL: string;
@staticDefault(5000)
static TIMEOUT: number;
}
console.log(Config.API_URL); // "http://localhost:3000"
console.log(Config.TIMEOUT); // 5000

参数装饰器在构造函数参数前调用:

function required(
target: any,
propertyKey: string | symbol,
parameterIndex: number
) {
// 保存参数索引
const index = parameterIndex;
// 在prototype上存储信息
const designType = Reflect.getMetadata("design:paramtypes", target, propertyKey);
// 存储required参数信息
}
function log(
target: any,
propertyKey: string | symbol,
parameterIndex: number
) {
console.log(`Parameter ${parameterIndex} of ${String(propertyKey)}`);
}
class UserService {
createUser(
@required name: string,
@log email: string
) {
console.log(`Creating user: ${name}, ${email}`);
}
}
// 依赖注入示例
const INJECTED_KEY = Symbol("INJECTED");
function inject(service: any) {
return function(
target: any,
propertyKey: string | symbol,
parameterIndex: number
) {
// 在构造函数参数上标记
const metadata = Reflect.getMetadata("params", target) || [];
metadata[parameterIndex] = { service };
Reflect.defineMetadata("params", metadata, target);
};
}
// 使用
class UserController {
constructor(
@inject(UserService) private userService: UserService
) {}
}

装饰器工厂是返回装饰器函数的函数:

// 普通装饰器
function sealed(target: Function) {
Object.seal(target);
}
// 装饰器工厂
function prefix(p: string) {
return function(target: Function) {
target.prototype.prefix = p;
};
}
// 使用
@prefix("TEST_")
class MyClass {}
console.log((new MyClass() as any).prefix); // "TEST_"
// 配置项
interface CacheOptions {
ttl: number;
key?: string;
}
function cache(options: CacheOptions) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
): PropertyDescriptor {
const original = descriptor.value;
const cache = new Map<string, { value: any; expire: number }>();
descriptor.value = function(...args: any[]) {
const key = options.key || `${propertyKey}:${JSON.stringify(args)}`;
if (cache.has(key)) {
const entry = cache.get(key)!;
if (entry.expire > Date.now()) {
return entry.value;
}
}
const result = original.apply(this, args);
cache.set(key, { value: result, expire: Date.now() + options.ttl });
return result;
};
return descriptor;
};
}
class DataService {
@cache({ ttl: 60000 }) // 60秒缓存
fetchData(id: string): string {
console.log("Fetching data...");
return `Data for ${id}`;
}
}
function ifEnv(env: string, decorator: Function) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
if (process.env.NODE_ENV === env) {
decorator(target, propertyKey, descriptor);
}
};
}
// 生产环境记录日志,开发环境忽略
class Api {
@ifEnv("production", log)
call() {
console.log("API called");
}
}

11.6 反射元数据(reflect-metadata)

Section titled “11.6 反射元数据(reflect-metadata)”

reflect-metadata允许在装饰器中读取和写入元数据:

Terminal window
npm install reflect-metadata
import "reflect-metadata";
// 定义元数据
Reflect.defineMetadata("custom:key", "value", target);
Reflect.defineMetadata("custom:key", "value", target, propertyKey);
// 读取元数据
const value = Reflect.getMetadata("custom:key", target);
const propValue = Reflect.getMetadata("custom:key", target, propertyKey);
// 检查是否有元数据
const hasMetadata = Reflect.hasMetadata("custom:key", target);

TypeScript使用以下design元数据(需要emitDecoratorMetadata: true):

  • design:type - 属性类型
  • design:paramtypes - 参数类型
  • design:returntype - 返回类型
import "reflect-metadata";
function logType(target: any, propertyKey: string) {
const type = Reflect.getMetadata("design:type", target, propertyKey);
console.log(`${propertyKey} type: ${type?.name}`);
}
class User {
@logType
name: string;
@logType
age: number;
}
import "reflect-metadata";
const VALIDATION_KEY = Symbol("validation");
interface ValidationRule {
validate(value: any): boolean;
message: string;
}
function Required(target: any, propertyKey: string) {
const rules = Reflect.getMetadata(VALIDATION_KEY, target, propertyKey) || [];
rules.push({
validate: (v: any) => v !== null && v !== undefined,
message: `${String(propertyKey)} is required`
});
Reflect.defineMetadata(VALIDATION_KEY, rules, target, propertyKey);
}
function MinLength(min: number) {
return function(target: any, propertyKey: string) {
const rules = Reflect.getMetadata(VALIDATION_KEY, target, propertyKey) || [];
rules.push({
validate: (v: string) => v.length >= min,
message: `${String(propertyKey)} must be at least ${min} characters`
});
Reflect.defineMetadata(VALIDATION_KEY, rules, target, propertyKey);
};
}
class User {
@Required
name: string;
@Required
@MinLength(8)
password: string;
}
function validate(obj: any): string[] {
const errors: string[] = [];
for (const key of Object.keys(obj)) {
const rules = Reflect.getMetadata(VALIDATION_KEY, obj, key) || [];
for (const rule of rules) {
if (!rule.validate(obj[key])) {
errors.push(rule.message);
}
}
}
return errors;
}
const user = new User();
user.name = "Alice";
user.password = "123"; // 太短
console.log(validate(user));
// ["password must be at least 8 characters"]

TypeScript支持两套装饰器:

  1. 实验性装饰器(当前默认)
  2. TC39标准化装饰器(ES2024+,需useDefineForClassFields: false)
// TC39装饰器(未来标准)
class User {
// 访问器装饰器
accessor name: string = "";
// 工厂装饰器
@logger
greet() {
console.log("Hello!");
}
}
// 实验性装饰器元数据
@reflectable
class User {
@format("YYYY-MM-DD")
createdAt: Date;
@range(0, 120)
age: number;
}
// 当前代码(实验性)
@sealed
class User {
@format("YYYY-MM-DD")
createdAt: Date;
}
// 未来代码(TC39)
class User {
@format("YYYY-MM-DD")
accessor createdAt: Date;
}

function log(target: any, methodName: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`[LOG] Calling ${methodName} with`, args);
const result = original.apply(this, args);
console.log(`[LOG] ${methodName} returned`, result);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
function timing(target: any, methodName: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = async function(...args: any[]) {
const start = performance.now();
const result = await original.apply(this, args);
const duration = performance.now() - start;
console.log(`${methodName} took ${duration.toFixed(2)}ms`);
return result;
};
return descriptor;
}
class DataService {
@timing
async fetchData() {
// 模拟网络请求
await new Promise(resolve => setTimeout(resolve, 100));
return { data: "value" };
}
}
function serializable(target: any) {
target.prototype.toJSON = function() {
const result: Record<string, any> = {};
for (const key of Object.keys(this)) {
const value = this[key];
if (value instanceof Date) {
result[key] = value.toISOString();
} else if (typeof value === "object" && value !== null) {
result[key] = value.toJSON ? (value as any).toJSON() : value;
} else {
result[key] = value;
}
}
return result;
};
}
@serializable
class User {
name: string;
createdAt: Date;
address: { city: string };
}
const user = new User();
user.name = "Alice";
user.createdAt = new Date("2024-01-01");
user.address = { city: "Beijing" };
console.log(JSON.stringify(user));

Python对比:

import json
from dataclasses import dataclass, asdict
@dataclass
class User:
name: str
created_at: datetime
address: dict
def to_json(self):
return {
**asdict(self),
"created_at": self.created_at.isoformat()
}

本章学习了TypeScript的装饰器与元编程:

  1. 装饰器基础:

    • 使用@decorator语法
    • 需要experimentalDecorators: true
    • 装饰器函数接收target等信息
  2. 类装饰器:

    • 接收构造函数
    • 可以修改类行为
    • 支持装饰器工厂
  3. 方法装饰器:

    • 接收descriptor
    • 可以修改方法行为
    • 访问器装饰器类似
  4. 属性装饰器:

    • 直接修改属性定义
    • 常用于默认值
  5. 参数装饰器:

    • 接收parameterIndex
    • 常用于依赖注入
  6. 装饰器工厂:

    • 返回装饰器的函数
    • 传递配置参数
  7. 反射元数据:

    • 使用reflect-metadata库
    • 定义和读取元数据
    • 实现复杂装饰器

创建一个@singleton装饰器,确保类只有一个实例。

创建一个@debounce(delay)装饰器,延迟方法调用。

创建一个@default(value)装饰器,设置默认值。

使用reflect-metadata实现字段验证装饰器。

创建一个完整的日志装饰器,记录方法调用和返回值。



下一章我们将学习TypeScript的类型编程深度,包括类型运算、infer关键字、递归类型计算,以及类型级别的FizzBuzz。