typescript - Härled typen på en parameter baserat på en tidigare parameter
Låt oss anta att jag i min kodbas har två olika implementationer av en EventEmitter. Till exempel något som detta:
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;
...
};
Låt oss nu också anta att jag har en JS-modul som kan arbeta med båda implementeringarna:
Något som:
class EventDisposer {
registerEvent(sender, eventName, handler) {
sender.on(eventName, handler);
}
...
}
Denna modul används över hela kodbasen (blandning av TS och JS). Jag vill konvertera denna modul från JS till TS.
Grundläggande försök:
type Sender = EventEmitterA | EventEmitterB;
type Handler = HandlerA | HandlerB;
class EventDisposer {
registerEvent(sender: Sender, eventName: string, handler: Handler) {
sender.on(eventName, handler);
}
}
Detta fungerar inte på grund av: Argument av typen 'Handler' kan inte tilldelas parametern av typen 'HandlerA & HandlerB'.
Det jag skulle vilja göra är att härleda typen av Handler baserat på typen av Sender (Emitter) så att jag inte behöver ändra något på anroparens sida. Något i stil med:
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);
}
}
Men även detta fungerar inte på grund av: Argument av typen 'H' kan inte tilldelas en parameter av typen 'HandlerA & HandlerB'.
Är det möjligt att begränsa typen av hanteraren baserat på typen av avsändaren? Jag antar att jag kan använda en typvakt inuti registerEvent.
Fullständigt exempel: https://tsplay.dev/wQ8o7W
Tack!
Svar #1
Ditt försök var nära, ditt misstag var mest bara ordningen på operationerna, men här är några tips.
För det första skulle jag skapa en generisk handlare för dina handtag, skapa handtagen, göra en typunion av dem, och sedan en generisk avsändare som tar ett handtag från den unionen.
För det andra kan din event-disposer-klass enkelt ta en hanterare, och använda den internt för att skapa en emitter åt dig.
På det här sättet är allt garanterat att vara av en enda typ neråt i trädet, här är mitt exempel:
// 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) => {});
Förmodligen det enklaste sättet att gå tillväga på.
Observera också, om du har många olika handlare kan du alltid bara vara lat och låta funktionen själv automatiskt härleda alla typer åt dig samtidigt som du behåller tidigare funktionalitet med knappt någon skillnad i kompileringen, till exempel:
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) => {});
Svar #2
Vad sägs om detta?
https://www.typescriptlang.org/play?#code/C4TwDgpgBAEghgOwCYBsICcA8AVAfFAXigAok5g4AuKbASkPwDcB7ASyQG4AoL0SKAKKMICYAIC2rYMAw58RAN5coUZgmIRhogHJxxEagGdg6VggDmAGigALRKgzV4yNFjy1qLdlwC+PAMYocIaGglrAACKshmDMhhhQSiroEObRMuhCIsA4UBAAHjLIoQgAruIARhgAPhXMzGiIuMTxyI5h2RJSGXLWmtm6+kYmZla29q5OE7LuicoqUK1IGAB0ahrhgxDWdi4YtNwqfn5c-mrGeYxRMYRQCBAA7h2i17Hx6MQHp+fA43vo2nKU3+mDKlQw8hIZAo1DBVXQHigXiQDESPm4ZwQF12DnQACF6ihgbjMHUGhAmrdSOQqFAyY0EIjkaiFOiAj9FiJlgCgc8xJJpLI4RDbklVAhqBsBnoDIsRhYdtN0MTXKDyvDcEy2CiCPhWb5vljfksMASGtQsqIuoKsPSKQhIWK1JL+joZcNTAq-riVbI7U0tewWcceJpXisUmljBhLcAWlyMIDxNYAOQpxX-JNfMPRMAR1LpGPheNtfGE1Pp72uM0oL5cAD09cE+Ug-hkKIw6GY6FCSFK0GAzCrGHrJvQUD40EkhnE5H8Ni4OZi+ajGVjJe5SYrGdxNezV1zK8LmWLY5r2+HPPEByAA
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);
