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相关问答推荐

为什么Typescript 将const翻译成var?

TypeScript 推断数组 [index]

交叉接口型防护装置

如何正确键入返回路由的函数对象?

在 Typescript 中使用 map / filter 处理过滤类型

来自通用参数的Typescript 对象键

强制对象键的类型

如何在 TypeScript 中动态使用一个接口的键?

只接受给定对象的键

TypeScript 函数返回可为空的 Promise

Angular2: Uncaught (in promise): 报价不支持判断!在组件中

如何轻松地将 typescript 变量与 html 标记中的字符串连接起来?

如何在 Angular 应用程序中检测与 rxjs 相关的内存泄漏

使用ts-node执行时使用Typescript导入nodejs`fs`?

Typescript,'NodeListOf' 不是数组类型或字符串类型

ERROR 错误:未捕获(在promise中),无法匹配任何路由.网址段

对类方法使用粗箭头语法时,我可以使用 TypeScript 重载吗?

无法在 VS Code 中调试当前的Typescript文件,因为找不到相应的 JavaScript

Angular测试如何防止ngOnInit调用直接测试方法

Typescript 中的枚举类型是什么?