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

TypeScript模拟addEventListener时类型不兼容问题求解

实现类型安全的EventTarget.addEventListener简化版

问题描述

尝试实现类似EventTarget.addEventListener的类型安全行为,在switch分支中已知事件类型和对应监听器类型的情况下,调用监听器时出现类型错误:EventA无法赋值给EventMap[K],提示EventA缺少EventB的time属性。需要在不使用any等强制类型转换的前提下,安全调用匹配类型的监听器(实际场景并非添加监听器时立即调用)。

原代码

class EventA extends Event { public readonly name: string = ''; }
class EventB extends Event { public readonly time: number = 0; }

interface EventMap {
  'event-a': EventA;
  'event-b': EventB;
}

function addEventListener<K extends keyof EventMap>(
  type: K, listener: (event: EventMap[K]) => void
) {
  // 模拟事件触发行为
  let eventA: EventA = null as any; // 实际场景中为真实生成的实例
  let eventB: EventB = null as any;

  switch (type) {
    case 'event-a':
      listener(eventA); // <-- 类型错误
      break;

    case 'event-b':
      listener(eventB); // <-- 类型错误
      break;
  }
}

错误信息

Argument of type 'EventA' is not assignable to parameter of type 'EventMap[K]'.
  Type 'EventA' is not assignable to type 'EventA & EventB'.
    Property 'time' is missing in type 'EventA' but required in type 'EventB'.

原因分析

泛型参数K代表keyof EventMap的联合类型,TypeScript无法在switch分支中自动将K缩小为具体的字面量类型,因此会将EventMap[K]推断为EventA | EventB的交叉类型(因为K可能是任意一个键),导致传入具体事件实例时类型不匹配。

解决方案

方案1:使用事件处理映射对象

通过创建一个带as const断言的映射对象,将事件类型与对应的事件生成、监听器调用逻辑绑定,让TypeScript准确推断每个分支的类型:

class EventA extends Event { public readonly name: string = ''; }
class EventB extends Event { public readonly time: number = 0; }

interface EventMap {
  'event-a': EventA;
  'event-b': EventB;
}

function addEventListener<K extends keyof EventMap>(
  type: K, listener: (event: EventMap[K]) => void
) {
  // 定义事件处理映射,as const 确保类型字面量不被拓宽
  const eventHandlers = {
    'event-a': () => {
      const eventA: EventA = null as any; // 实际场景替换为真实事件实例生成逻辑
      listener(eventA);
    },
    'event-b': () => {
      const eventB: EventB = null as any; // 实际场景替换为真实事件实例生成逻辑
      listener(eventB);
    }
  } as const;

  // 调用对应事件的处理逻辑
  eventHandlers[type]();
}

方案2:提取分支逻辑到专用函数

将每个事件类型的处理逻辑提取到类型明确的专用函数中,利用TypeScript的类型缩小特性自动匹配监听器类型:

class EventA extends Event { public readonly name: string = ''; }
class EventB extends Event { public readonly time: number = 0; }

interface EventMap {
  'event-a': EventA;
  'event-b': EventB;
}

// 处理event-a的专用函数
function handleEventA(listener: (event: EventA) => void) {
  const eventA: EventA = null as any; // 实际场景替换为真实事件实例生成逻辑
  listener(eventA);
}

// 处理event-b的专用函数
function handleEventB(listener: (event: EventB) => void) {
  const eventB: EventB = null as any; // 实际场景替换为真实事件实例生成逻辑
  listener(eventB);
}

function addEventListener<K extends keyof EventMap>(
  type: K, listener: (event: EventMap[K]) => void
) {
  switch (type) {
    case 'event-a':
      handleEventA(listener);
      break;
    case 'event-b':
      handleEventB(listener);
      break;
  }
}

两种方案都能在不使用any强制转换的前提下,让TypeScript正确推断监听器与事件实例的类型匹配关系,实现类型安全的事件触发。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.31 20:01:33