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

TypeScript泛型类中如何正确实现emit方法重载?

TypeScript泛型类中EventEmitter的emit方法重载正确写法

需求很明确:泛型EventEmitter类的emit方法要根据事件类型自动校验参数数量——如果对应事件是无参函数,emit只需要传事件名;如果是带参函数,必须传入匹配Parameters<T[evt]>类型的参数数组。之前尝试的重载写法没达到预期,以下是正确实现方式。

现有代码

interface IEventListeners {
  [key: string]: (...args: any[]) => void | false
}

export class EventEmiiter<
  T extends IEventListeners
>  {
  private _listeners: {
    [K in keyof T]?: T[K][]
  } = {};

  public on<E extends keyof T>(evt: E, executor: T[E]) {
    const listener = this.getListener(evt);
    listener.push(executor);
    return () => {
      this.off(evt, executor);
    }
  }

  public off<E extends keyof T>(evt: E, executor: T[E]) {
    const listener = this.getListener(evt);
    const index = listener.findIndex(exe => exe === executor);
    listener.splice(index, 1);
  }

  public emit<E extends keyof T>(evt: E, params?: Parameters<T[E]>)  {
    const listener = this.getListener(evt);
    const iter = listener[Symbol.iterator]();
    let iterObj: ReturnType<typeof iter['next']>;
    let falseValue: undefined | false;
    while (iterObj = iter.next()) {
      const result = iterObj.value(...(params || []))
      if (result === false) {
        falseValue = false;
      }
      if (iterObj.done) {
        break;
      }
    }
    return falseValue;
  }

  private getListener<E extends keyof T>(evt: E) {
    return this._listeners[evt] = this._listeners[evt] || [];
  }

  /** 
   * React Component Hook 
   * @describe 用于订阅的hooks, 不需考虑事件的卸载, 组件卸载时自动卸载事件 
   * */
  public useSubscribe<E extends keyof T>(evt: E, executor: T[E]) {
    const off = this.on(evt, executor);
    
    useUnmount(() => {
      off();
    })

    return off;
  }
}


type Subscribers = {
  onOk: () => void;
  onClick: (val: number) => void;
}
const emitter = new EventEmiiter<Subscribers>;

emitter.emit('onOk') // 这里期望不报错,onOk是无参函数
emitter.emit('onClick'); // 这里期望报错,onClick需要参数

尝试的重载声明(未生效)

class EventEmitter {
  public emit<E extends keyof T>(evt: E)
  public emit<E extends keyof T>(evt: E, params: Parameters<T[E]>)
  public emit<E extends keyof T>(evt: E, params?: Parameters<T[E]>)
}

正确实现方案

核心是通过条件类型区分无参和带参事件,让TypeScript能根据事件名自动推断所需参数:

  1. 先修正类名拼写错误(EventEmiiter → EventEmitter)
  2. 为emit方法定义两个重载签名,分别对应无参和带参场景
  3. 实现签名保持兼容,但通过条件类型约束参数的可选性

完整修正代码

interface IEventListeners {
  [key: string]: (...args: any[]) => void | false
}

export class EventEmitter<T extends IEventListeners> {
  private _listeners: {
    [K in keyof T]?: T[K][]
  } = {};

  public on<E extends keyof T>(evt: E, executor: T[E]) {
    const listener = this.getListener(evt);
    listener.push(executor);
    return () => {
      this.off(evt, executor);
    };
  }

  public off<E extends keyof T>(evt: E, executor: T[E]) {
    const listener = this.getListener(evt);
    const index = listener.findIndex(exe => exe === executor);
    if (index !== -1) {
      listener.splice(index, 1);
    }
  }

  // 重载1:无参事件,只传事件名
  public emit<E extends keyof T>(
    evt: E
  ): Parameters<T[E]> extends [] ? void : never;

  // 重载2:带参事件,必须传事件名+参数数组
  public emit<E extends keyof T>(
    evt: E,
    params: Parameters<T[E]>
  ): Parameters<T[E]> extends [] ? never : void;

  // 实现签名,兼容所有重载
  public emit<E extends keyof T>(evt: E, params?: Parameters<T[E]>) {
    const listener = this.getListener(evt);
    let falseValue: undefined | false;
    for (const handler of listener) {
      const result = handler(...(params || []));
      if (result === false) {
        falseValue = false;
      }
    }
    return falseValue;
  }

  private getListener<E extends keyof T>(evt: E): T[E][] {
    return (this._listeners[evt] = this._listeners[evt] || []);
  }

  public useSubscribe<E extends keyof T>(evt: E, executor: T[E]) {
    const off = this.on(evt, executor);
    
    // 假设useUnmount是已定义的React钩子
    useUnmount(() => {
      off();
    });

    return off;
  }
}

type Subscribers = {
  onOk: () => void;
  onClick: (val: number) => void;
};

const emitter = new EventEmitter<Subscribers>();

emitter.emit('onOk'); // ✅ 正确,无参事件不需要传参数
emitter.emit('onClick'); // ❌ 报错:缺少必需的参数params
emitter.emit('onClick', [123]); // ✅ 正确,参数类型匹配
emitter.emit('onOk', []); // ❌ 报错:无参事件不能传参数

关键说明

  • 重载签名通过Parameters<T[E]> extends []判断事件是否无参,TypeScript会根据传入的事件名自动匹配对应的重载
  • 实现签名中的params?只是为了兼容所有重载,实际调用时TS会根据事件类型强制要求是否传参数
  • 修正了原代码中while循环的逻辑,改用for...of更简洁可靠

内容的提问来源于stack exchange,提问作者tttinkl

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 07:15:55