Let's assume that in my codebase I have two different implementations of an EventEmitter. For example something like this:

type HandlerA = (data: boolean) => void;

type HandlerB = (data: number) => void; // HandlerB is somehow different from HandlerA

type EventEmitterA = {
  on(eventName: string, handler: HandlerA): void;
  ...
};

type EventEmitterB = {
  on(eventName: string, handler: HandlerB): void;
  ...
};

Now let's also assume I have a JS module that is able to work with both implementations:

Something like:

class EventDisposer {
  registerEvent(sender, eventName, handler) {
    sender.on(eventName, handler);
  }
  ...
}

This module is used across the codebase (mix of TS and JS). I want to convert this module from JS to TS.

Basic attemp:

type Sender = EventEmitterA | EventEmitterB;

type Handler = HandlerA | HandlerB;

class EventDisposer {
  registerEvent(sender: Sender, eventName: string, handler: Handler) {
    sender.on(eventName, handler);
  }
}

This doesn't work because of: Argument of type 'Handler' is not assignable to parameter of type 'HandlerA & HandlerB'.

What I would like to do is infer the type of Handler based on the type of the Sender (Emitter) so I don't need to change anything on the caller side. Something like:

type ExtractHandler<S extends Sender> = S extends EventEmitterA ? HandlerA : HandlerB;

type handlerA = ExtractHandler<EventEmitterA>;

type handlerB = ExtractHandler<EventEmitterB>;

class EventDisposer2 {
  registerEvent<S extends Sender, H = ExtractHandler<S>>(sender: S, eventName: string, handler: H) {
    sender.on(eventName, handler);
  }
}

But also this doesn't work because of: Argument of type 'H' is not assignable to parameter of type 'HandlerA & HandlerB'.

Is it possible to narrow the type of the handler based on the type of the sender? I guess I can use a type guard inside registerEvent.

Full example: https://tsplay.dev/wQ8o7W

Thanks!

推荐答案

Your attempt was close, your error was mostly just order of operations, but here are some tips.

Firstly I would make a generic handler for your handles, create the handles, type union them, then a generic emitter which takes a handle from that union.

Secondly your event disposer class can simple take a handler, and using it internally type an emitter for you.

This way everything is guaranteed to be a single type down the tree, here is my example:

// Generic handler (why rewrite the same type code
// multiple times when we have generics?)
type GenericHandler<T> = (data: T) => void;

type HandlerA = GenericHandler<boolean>;
type HandlerB = GenericHandler<number>;

// Handler Union
type Handlers = HandlerA | HandlerB;

// Generic emitter that uses the union
interface GenericEmitter<T extends Handlers> {
  on(eventName: string, handler: T): void
}

// Event disposer which simply takes the handler type.
class EventDisposer {
  registerEvent<T extends Handlers>(sender: GenericEmitter<T>, eventName: string, handler: T) {
    sender.on(eventName, handler);
  }
}

// Here is the example usage, you also do not
// need to add types to parameters because typescript is already aware.
new EventDisposer().registerEvent<HandlerA>({ on: (eventName, handler) => {} }, "", (data) => {});

Probably the simplest way in going about it.

Also note, if you have many different handlers you can always just be lazy and have the function itself automatically infer all the types for you whilst still retaining previous functionality with barely any difference in compilation ex:

type GenericHandler<T = any> = (data: T) => void;

interface GenericEmitter<T extends GenericHandler> {
  on(eventName: string, handler: T): void
}

class EventDisposer {
  registerEvent<T extends GenericHandler>(sender: GenericEmitter<T>, eventName: string, handler: T) {
    sender.on(eventName, handler);
  }
}

// `data: boolean` creates a handler, it will infer it all for you (passing functions work too).
new EventDisposer().registerEvent({ on: (eventName, handler) => {} }, "", (data: boolean) => {});

Typescript相关问答推荐

属性的类型,该属性是类的键,其值是所需类型

我们过go 不能从TypeScript中的联合中同时访问所有属性?

如何在使用`Next—intl`和`next/link`时导航

如何在TypeScript中将一个元组映射(转换)到另一个元组?

在包含泛型的类型脚本中引用递归类型A<;-&B

react 路由Use RouteLoaderData类型脚本错误

获取一个TypeScrip对象,并将每个属性转换为独立对象的联合

Angular 17子路由

接口重载方法顺序重要吗?

从route.参数获取值.订阅提供";无法读取未定义";的属性

将对象的属性转换为正确类型和位置的函数参数

当数组类型被扩展并提供标准数组时,TypeScrip不会抱怨

埃斯林特警告危险使用&as";

使用&reaction测试库&如何使用提供程序包装我的所有测试组件

更漂亮的格式数组<;T>;到T[]

TypeScript是否不知道其他函数内部的类型判断?

Typescript 和 Rust 中跨平台的位移数字不一致

如何确定单元格中的块对 mat-h​​eader-cell 的粘附(粘性)?

React TypeScript react-hook-form - 字符串类型的参数不可分配给类型的参数

扩展类型以允许附加字段