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

如何为EventEmitter或EventTarget编写支持类型推断的泛型函数?

通用类型安全addListener实现方案

核心思路是利用TypeScript的条件类型和泛型推导,自动从任意事件发射器实例上提取「事件名-回调参数」的映射关系,不需要调用方手动传入泛型参数,就能达到和原生addEventListener一致的类型提示效果。


核心类型与实现

// 事件监听配置项,对齐原生标准同时支持自定义扩展
type ListenerOptions = {
  once?: boolean;
  signal?: AbortSignal;
  capture?: boolean;
};

// 工具类型:自动从事件发射器实例提取事件-回调映射表
type ExtractEventMap<T> = 
  // 匹配标准DOM EventTarget派生类型(如HTMLElement、WebSocket等自带内置事件类型的对象)
  T extends { addEventListener<K extends keyof infer EMap>(
    type: K,
    listener: (ev: EMap[K]) => any,
    options?: any
  ): void }
    ? EMap
    : // 匹配Node.js/自定义风格EventEmitter(通常通过on方法注册事件,带事件名泛型)
    T extends { on<K extends keyof infer EMap>(
      event: K,
      listener: (payload: EMap[K]) => void
    ): void }
    ? EMap
    : // 兜底通用Event类型
    Record<string, Event>;

function addListener<
  TTarget,
  TEventName extends keyof ExtractEventMap<TTarget>
>(
  target: TTarget,
  eventName: TEventName,
  callback: (event: ExtractEventMap<TTarget>[TEventName]) => void,
  options?: ListenerOptions
): void {
  // once、signal等能力polyfill逻辑
  let wrappedHandler = callback as (...args: any[]) => void;
  const removeHandler = () => {
    if ('removeEventListener' in target) {
      (target as EventTarget).removeEventListener(
        eventName as string,
        wrappedHandler as EventListener,
        options?.capture
      );
    } else if ('off' in target) {
      (target as any).off(eventName, wrappedHandler);
    } else if ('removeListener' in target) {
      (target as any).removeListener(eventName, wrappedHandler);
    }
  };

  if (options?.once) {
    const originHandler = wrappedHandler;
    wrappedHandler = (...args: any[]) => {
      originHandler(...args);
      removeHandler();
    };
  }

  if (options?.signal) {
    if (options.signal.aborted) return;
    options.signal.addEventListener('abort', removeHandler, { once: true });
  }

  // 注册事件,兼容不同发射器的API
  if ('addEventListener' in target) {
    (target as EventTarget).addEventListener(
      eventName as string,
      wrappedHandler as EventListener,
      options
    );
  } else if ('on' in target) {
    (target as any).on(eventName, wrappedHandler);
  } else if ('addListener' in target) {
    (target as any).addListener(eventName, wrappedHandler);
  }
}

使用效果

1. 原生DOM EventTarget场景

不需要手动标注类型,回调参数自动推导:

const button = document.createElement('button');
addListener(button, 'click', (event) => {
  // event自动推导为MouseEvent,所有属性、方法都有类型提示
  console.log(event.clientX, event.clientY);
});

2. 自定义EventEmitter场景

只要你的自定义发射器的注册方法(addEventListener/on/addListener)携带正确的事件泛型定义,就能自动推导参数类型:

// 自定义事件类型映射
type AppEventMap = {
  message: { id: string; content: string; timestamp: number };
  error: Error;
  close: number;
};

// 自定义EventEmitter实现
class AppEventEmitter {
  on<K extends keyof AppEventMap>(event: K, handler: (payload: AppEventMap[K]) => void) {}
  off<K extends keyof AppEventMap>(event: K, handler: (payload: AppEventMap[K]) => void) {}
}

const appEmitter = new AppEventEmitter();
addListener(appEmitter, 'message', (payload) => {
  // payload自动推导为{ id: string; content: string; timestamp: number }
  console.log(payload.id, payload.timestamp);
});

说明

  • 类型推导完全自动,调用侧不需要写任何any类型,也不需要手动传入泛型参数
  • 兼容三类事件发射器:标准DOM EventTarget、Node.js原生EventEmitter、自定义类EventEmitter结构
  • 内部仅在polyfill适配层使用了少量类型断言,对外API完全类型安全
  • 可以根据自己的业务需求扩展ListenerOptions的字段,适配更多事件配置能力

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 06:36:16