You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 10:15:58