TypeScript 5.0+中如何通过装饰器更新类实例属性?
问题与解决方案:类装饰器注册动作-方法映射的类型校验问题
需求目标
通过方法装饰器传入的action值关联类方法,将动作-方法映射存储在类实例中,实现方法“注册”到映射表(例如{ "send": sendMessage(), ... })。
问题描述
在类中显式声明actionsMap属性(带类型标注)后,装饰器中更新该属性时,映射表始终为空;移除该属性后代码可正常运行,但失去TypeScript类型校验。需要解决这一矛盾,同时寻求更优实现方案。
可运行代码示例
enum Action { Send, Create, }; function action(action: Action) { return function <This, Args extends any[], Return>( target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return> ) { if (context.kind !== 'method') throw new Error('This decorator can only be applied to methods.'); context.addInitializer(function () { if (!this.actionsMap) this.actionsMap = new Map(); this.actionsMap.set(action, target.bind(this)); }); }; } class Service { // 显式声明后映射表为空: // actionsMap: Map<Action, Function> = new Map(); @action(Action.Send) public sendMessage() { console.log('Sending message...'); } @action(Action.Create) public createMessage() { console.log('Creating message...'); } } const service = new Service(); service.actionsMap.get(Action.Send)();
解决方案
1. 修复现有代码的类型校验问题
问题根源是类属性初始化顺序:类中显式初始化actionsMap = new Map()会覆盖装饰器初始化器中创建的映射表——类字段初始化逻辑优先于装饰器的addInitializer回调执行。
解决方法:仅声明属性类型,不手动初始化,让装饰器负责初始化工作:
class Service { // 仅声明类型,不初始化 actionsMap: Map<Action, (...args: any[]) => void>; @action(Action.Send) public sendMessage() { console.log('Sending message...'); } @action(Action.Create) public createMessage() { console.log('Creating message...'); } }
这样既保留了TypeScript的类型校验能力,又不会覆盖装饰器中创建的映射表。
2. 更优实现方案:自动推断方法类型的装饰器
通过泛型让装饰器自动推断注册方法的参数和返回值类型,同时给类添加更精确的类型约束,避免手动维护映射表的类型定义:
实现代码
enum Action { Send, Create, }; // 定义动作映射的类型,自动关联类方法类型 type ActionMap<T> = { [K in Action]: T[keyof T] extends (...args: any[]) => infer R ? (...args: Parameters<T[keyof T]>) => R : never; }; function action<T extends object>(action: Action) { return function <Args extends any[], Return>( target: (this: T, ...args: Args) => Return, context: ClassMethodDecoratorContext<T, (this: T, ...args: Args) => Return> ) { if (context.kind !== 'method') throw new Error('This decorator can only be applied to methods.'); context.addInitializer(function (this: T & { actionsMap: ActionMap<T> }) { if (!this.actionsMap) { this.actionsMap = {} as ActionMap<T>; } (this.actionsMap as any)[action] = target.bind(this); }); }; } // 定义包含动作映射的类类型 type ActionService<T> = T & { actionsMap: ActionMap<T> }; class Service { @action(Action.Send) public sendMessage() { console.log('Sending message...'); } @action(Action.Create) public createMessage(content: string) { console.log(`Creating message: ${content}`); } } // 类型断言让TypeScript识别actionsMap属性 const service = new Service() as ActionService<Service>; // 此时会有精确的类型提示,比如调用createMessage时要求传入string参数 service.actionsMap[Action.Create]('Hello World');
方案优势
- 自动推断方法的参数和返回值类型,提供精确的类型校验与编辑器提示
- 无需手动维护
actionsMap的类型定义,减少代码冗余 - 支持带参数的方法注册,类型约束更严谨
内容的提问来源于stack exchange,提问作者Karolis
相关产品推荐
相关产品推荐

