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

如何在辅助函数中映射泛型Record键值时保留类型推断?

类型化事件发射器的辅助订阅函数类型推断问题

我有一个类型化事件发射器:

type Listener<T> = (arg: T) => void;
class TypedEventEmitter<Events extends {}> {
    public on<E extends keyof Events>(event: E, listener: Listener<Events[E]>) {
        // ...
    }
}

它的使用方式如下:

type MyEvents =  {
    click: { x: number, y: number };
    keydown: { code: string };
}
class MyClass extends TypedEventEmitter<MyEvents> {}
const myClass = new MyClass();
myClass.on('click', data => console.log(data.x)); // 正常工作
myClass.on('keydown', data => console.log(data.x)); // 错误:类型“{ code: string; }”上不存在属性“x”

目前一切正常,但我想创建一个辅助函数来订阅该事件发射器,使用方式如下:

subscribe(myClass, 'click', (data) => { /* ... */ });

我的尝试如下:

function subscribe<
    T extends TypedEventEmitter<Events>,
    Events extends {},
    E extends keyof Events
>(eventEmitter: T, eventName: E, listener: Listener<Events[E]>) {
   eventEmitter.on(eventName, listener);
}

如果显式指定类型参数,它可以正常工作:

subscribe<MyClass, MyEvents, 'click'>(myClass, 'click', (data) => {
    console.log(data.x);
});

但直接使用会报错,无法自动推断类型参数:

subscribe(myClass, 'click', (data) => {
                // ^ 类型“"click"”的参数不能赋给类型“never”的参数
});

我还尝试了另一种写法,但也存在问题:

function subscribe2<
    T extends TypedEventEmitter<{}>
>(eventEmitter: T, ...[eventName, listener]: Parameters<T['on']>) {
    eventEmitter.on(eventName, listener);
}

subscribe2(myClass, 'click', (data) => {
    console.log(data.x);
                  // ^ 类型“{ x: number; y: number; } | { code: string; }”上不存在属性“x”
});

有没有办法无需显式指定类型参数就能实现该功能?


解决方案

可以通过调整泛型参数的约束和顺序,让TypeScript正确自动推断类型:

方法一:调整泛型参数顺序

将Events作为第一个泛型参数,让TypeScript先从事件发射器实例推断出事件类型,再推导具体事件名和监听器类型:

function subscribe<
    Events extends {},
    E extends keyof Events,
    T extends TypedEventEmitter<Events>
>(eventEmitter: T, eventName: E, listener: Listener<Events[E]>) {
    eventEmitter.on(eventName, listener);
}

调用时即可自动推断类型:

subscribe(myClass, 'click', (data) => {
    console.log(data.x); // 正确识别data为{ x: number, y: number }
});

subscribe(myClass, 'keydown', (data) => {
    console.log(data.code); // 正确识别data为{ code: string }
});

方法二:提取事件发射器的内置事件类型

使用条件类型提取TypedEventEmitter实例的事件类型,简化泛型定义:

type ExtractEvents<T> = T extends TypedEventEmitter<infer E> ? E : never;

function subscribe<
    T extends TypedEventEmitter<{}>,
    E extends keyof ExtractEvents<T>
>(eventEmitter: T, eventName: E, listener: Listener<ExtractEvents<T>[E]>) {
    eventEmitter.on(eventName, listener);
}

这种写法同样能实现自动推断,且无需显式声明Events参数。

原写法失败原因

  • 第一种尝试中,泛型参数顺序为T→Events→E,TypeScript无法从T反向推断出Events的具体类型,导致E被推断为never。
  • 第二种尝试中,Parameters<T['on']>会将所有事件的类型合并为联合类型,无法区分具体事件对应的参数类型。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 19:37:13