第11章 装饰器与元编程
本章定位:深入学习TypeScript的装饰器(Decorator)机制,包括类装饰器、方法装饰器、属性装饰器、参数装饰器,以及装饰器工厂和反射元数据的概念。
11.1 装饰器模式回顾
Section titled “11.1 装饰器模式回顾”什么是装饰器
Section titled “什么是装饰器”装饰器是一种设计模式,允许在不修改原类的情况下,给类或对象动态添加功能。
// 基础类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装饰器
Section titled “TypeScript装饰器”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 }}11.2 类装饰器
Section titled “11.2 类装饰器”基本类装饰器
Section titled “基本类装饰器”类装饰器接收构造函数作为参数:
// 记录所有创建的对象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;}
@loggedclass User { name: string;
constructor(name: string) { this.name = name; }}
const user = new User("Alice");// "Creating instance of User"类装饰器修改类
Section titled “类装饰器修改类”// 冻结类(禁止添加新属性)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@withVersionclass Config { apiUrl: string;}
console.log(Config.VERSION); // "1.0.0"Config.NEW_PROP = "value"; // 编译或运行时可能报错(取决于实现)带参数的类装饰器
Section titled “带参数的类装饰器”// 装饰器工厂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@secondclass MyClass {}
// 输出:second first(由下到上)11.3 方法与属性装饰器
Section titled “11.3 方法与属性装饰器”方法装饰器接收方法描述符:
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; // 错误!方法不能修改访问器装饰器
Section titled “访问器装饰器”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; // OKaccount.balance = -50; // Error: balance must be non-negativefunction 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"static属性装饰器
Section titled “static属性装饰器”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); // 500011.4 参数装饰器
Section titled “11.4 参数装饰器”基本参数装饰器
Section titled “基本参数装饰器”参数装饰器在构造函数参数前调用:
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}`); }}参数装饰器与元数据
Section titled “参数装饰器与元数据”// 依赖注入示例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 ) {}}11.5 装饰器工厂
Section titled “11.5 装饰器工厂”装饰器工厂是返回装饰器函数的函数:
// 普通装饰器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_"带配置的装饰器工厂
Section titled “带配置的装饰器工厂”// 配置项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允许在装饰器中读取和写入元数据:
npm install reflect-metadataimport "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);内置元数据类型
Section titled “内置元数据类型”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;}实际应用:自动验证器
Section titled “实际应用:自动验证器”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"]11.7 TypeScript实验性装饰器
Section titled “11.7 TypeScript实验性装饰器”TC39装饰器提案
Section titled “TC39装饰器提案”TypeScript支持两套装饰器:
- 实验性装饰器(当前默认)
- TC39标准化装饰器(ES2024+,需
useDefineForClassFields: false)
标准装饰器语法
Section titled “标准装饰器语法”// TC39装饰器(未来标准)class User { // 访问器装饰器 accessor name: string = "";
// 工厂装饰器 @logger greet() { console.log("Hello!"); }}装饰器元数据提案
Section titled “装饰器元数据提案”// 实验性装饰器元数据@reflectableclass User { @format("YYYY-MM-DD") createdAt: Date;
@range(0, 120) age: number;}// 当前代码(实验性)@sealedclass User { @format("YYYY-MM-DD") createdAt: Date;}
// 未来代码(TC39)class User { @format("YYYY-MM-DD") accessor createdAt: Date;}11.8 实际应用场景
Section titled “11.8 实际应用场景”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; };}
@serializableclass 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 jsonfrom dataclasses import dataclass, asdict
@dataclassclass User: name: str created_at: datetime address: dict
def to_json(self): return { **asdict(self), "created_at": self.created_at.isoformat() }11.9 本章小结
Section titled “11.9 本章小结”本章学习了TypeScript的装饰器与元编程:
-
装饰器基础:
- 使用
@decorator语法 - 需要
experimentalDecorators: true - 装饰器函数接收target等信息
- 使用
-
类装饰器:
- 接收构造函数
- 可以修改类行为
- 支持装饰器工厂
-
方法装饰器:
- 接收descriptor
- 可以修改方法行为
- 访问器装饰器类似
-
属性装饰器:
- 直接修改属性定义
- 常用于默认值
-
参数装饰器:
- 接收parameterIndex
- 常用于依赖注入
-
装饰器工厂:
- 返回装饰器的函数
- 传递配置参数
-
反射元数据:
- 使用reflect-metadata库
- 定义和读取元数据
- 实现复杂装饰器
练习11.1:类装饰器
Section titled “练习11.1:类装饰器”创建一个@singleton装饰器,确保类只有一个实例。
练习11.2:方法装饰器
Section titled “练习11.2:方法装饰器”创建一个@debounce(delay)装饰器,延迟方法调用。
练习11.3:属性装饰器
Section titled “练习11.3:属性装饰器”创建一个@default(value)装饰器,设置默认值。
练习11.4:元数据验证器
Section titled “练习11.4:元数据验证器”使用reflect-metadata实现字段验证装饰器。
练习11.5:日志装饰器
Section titled “练习11.5:日志装饰器”创建一个完整的日志装饰器,记录方法调用和返回值。
下一章我们将学习TypeScript的类型编程深度,包括类型运算、infer关键字、递归类型计算,以及类型级别的FizzBuzz。