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!
CodePudding user response:
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) => {});
CodePudding user response:
What about this?
type Handler<T> = (data: T) => void;
type EventEmitter<T> = {
on(eventName: string, handler: Handler<T>): void
}
class EventDisposer {
registerEvent<T extends number|boolean>(sender: EventEmitter<T>, eventName: string, handler: Handler<T>) {
sender.on(eventName, handler);
}
}
const evDisp = new EventDisposer();
const handlerNum: Handler<number> = (data: number): void => {};
const handlerBool: Handler<boolean> = (data: boolean): void => {};
const senderNum: EventEmitter<number> = {
on: (eventName: string, handler: Handler<number>): void => {}
}
const senderBool: EventEmitter<boolean> = {
on: (eventName: string, handler: Handler<boolean>): void => {}
}
evDisp.registerEvent(senderNum, '', handlerNum);
evDisp.registerEvent(senderBool, '', handlerBool);
// Expected errors due to handler/sender type mismatch
evDisp.registerEvent(senderNum, '', handlerBool);
evDisp.registerEvent(senderBool, '', handlerNum);