Utbyggbar, starkt typad Event Emitter-gränssnitt i Typescript
Jag har använt ett starkt typat gränssnitt för event emitter i Typescript ett tag, men nu behöver jag att det ska stödja underklasser som lägger till egna event till det. Vid någon punkt misslyckas Typescript med att känna till basklassens event.
Här är koden i en kondenserad version (Playground-länk):
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type AddParameters<ListenersT, EventT> =
ListenersT extends (...args: infer ArgsT) => void
? (event: EventT, ...args: ArgsT) => Promise<boolean>
: never;
type EmitSignatures<ListenersT> =
{ [EventT in keyof ListenersT]: AddParameters<ListenersT[EventT], EventT> };
type EmitAll<ListenersT> = UnionToIntersection<EmitSignatures<ListenersT>[keyof ListenersT]>
type OnSignatures<ListenersT, ReturnT> =
{ [EventT in keyof ListenersT]: (event: EventT, listener: ListenersT[EventT]) => ReturnT };
type OnAll<ListenersT, ReturnT> =
UnionToIntersection<OnSignatures<ListenersT, ReturnT>[keyof ListenersT]>;
type EventEmitter<ListenersT> = EmitterInterface<ListenersT>;
export interface EmitterInterface<ListenersT>
{
emit: EmitAll<ListenersT>;
on: OnAll<ListenersT, this>;
}
/////////////////////////////////////////////////////////////////////////////////////////////
interface VehicleEvents
{
accelerate(acceleration: number): void;
brake(deceleration: number): void;
}
interface BusEvents extends VehicleEvents
{
doorStateChange(front: boolean, middle: boolean, rear: boolean): void
}
interface Vehicle<E extends VehicleEvents> extends EventEmitter<E>
{
onSig: OnSignatures<E, this>;
onSigs: OnSignatures<E, this>[keyof E];
}
class Vehicle<E extends VehicleEvents>
{
public constructor()
{ this.on('brake', () => this.flashBrakeLights()); } // supposed to work?
public flashBrakeLights(): void {}
public hitTheGas(strength: number): void
{ this.emit('accelerate', strength * 42); } // supposed to work?
public test(): void
{
this.onSig.accelerate;
this.onSig.brake;
this.onSigs('accelerate', (a) => undefined); // error I don't understand
this.onSigs('brake', (d) => undefined); // error I don't understand
this.onSigs('foo', () => undefined); // supposed to error
}
}
interface Bus extends EventEmitter<BusEvents> {}
class Bus extends Vehicle<BusEvents>
{
public doorState: [boolean, boolean, boolean] = [false, false, false];
public constructor()
{
super();
this.on('accelerate', () => {
this.door(0, false);
this.door(1, false);
this.door(2, false);
});
}
public door(index: number, state: boolean): void
{
this.doorState[index] = state;
this.emit('doorStateChange', ...this.doorState);
}
}
export const bus = new Bus();
Typen E deklareras som en utökning av VehicleEvents, vilket borde vara tillräckligt för att låta Typescript veta att det finns accelerate och brake händelser, eller hur?
Några förklaringar till varför detta inte fungerar? Några idéer om hur man kan fixa detta eller uppnå det jag behöver på ett annat sätt?
Svar #1
Problemet är att inom klassen kan de där fina villkorsbaserade typerna (inte säker på var de kom ifrån, host) inte lösas om de fortfarande innehåller olösta typparametrar. Så medan ditt tillvägagångssätt att använda en generisk typparameter för utbyggbarhet verkar som en bra idé, är effekten att det gör on och emit oanvändbara inom klassen.
En lösning är att inte använda en typparameter, använd bara själva händelsegränssnittet. Problemet med det (som du utan tvekan upptäckte) är att det gör klassen omöjlig att utöka, eftersom någon härledd version av on och emit inte kommer att vara kompatibel med bas typversionen.
För att åtgärda detta problem kan vi använda en funktion som tar bort on och emit från basklassen. Detta är lite klumpigt men jag tror inte att det finns ett bättre sätt.
interface VehicleEvents {
accelerate(acceleration: number): void;
brake(deceleration: number): void;
}
interface BusEvents extends VehicleEvents {
doorStateChange(front: boolean, middle: boolean, rear: boolean): void
}
interface Vehicle extends EventEmitter<VehicleEvents> {}
class Vehicle {
public constructor() {
this.on('brake', () => this.flashBrakeLights()); //ok
}
public flashBrakeLights(): void { }
public hitTheGas(strength: number): void { this.emit('accelerate', strength * 42); } // ok
}
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
interface Bus extends EventEmitter<BusEvents> { }
function extendEmitter<TBaseCtor extends new (...a: any[])=> any>(ctor: TBaseCtor){
return ctor as (new (...a: ConstructorParameters<TBaseCtor>) => Omit<InstanceType<TBaseCtor>, 'on' | 'emit'>)
}
class Bus extends extendEmitter(Vehicle) {
public doorState: [boolean, boolean, boolean] = [false, false, false];
public constructor() {
super();
this.on('accelerate', () => {
this.door(0, false);
this.door(1, false);
this.door(2, false);
});
}
public door(index: number, state: boolean): void {
this.doorState[index] = state;
this.emit('doorStateChange', ...this.doorState);
}
}
export const bus = new Bus();
Ovanstående version säkerställer inte att den nyutvecklade implementerar bas-händelsen på ett korrekt sätt. Vi kan skriva en version som skulle validera det men det skulle kräva en liten ändring av de ursprungliga definitionerna för att tillåta oss att extrahera händelsegränssnittet från basklassen:
type UnionToIntersection<U> =
(U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
type AddParameters<ListenersT, EventT> =
ListenersT extends (...args: infer ArgsT) => void
? (event: EventT, ...args: ArgsT) => Promise<boolean>
: never;
type EmitSignatures<ListenersT> =
{ [EventT in keyof ListenersT]: AddParameters<ListenersT[EventT], EventT> };
type EmitAll<ListenersT> = UnionToIntersection<EmitSignatures<ListenersT>[keyof ListenersT]>
type OnSignatures<ListenersT, ReturnT> =
{ [EventT in keyof ListenersT]: (event: EventT, listener: ListenersT[EventT]) => ReturnT };
type OnAll<ListenersT, ReturnT> =
UnionToIntersection<OnSignatures<ListenersT, ReturnT>[keyof ListenersT]>;
type EventEmitter<ListenersT> = EmitterInterface<ListenersT>;
export interface EmitterInterface<ListenersT>
{
emit: EmitAll<ListenersT>;
on: OnAll<ListenersT, this> & {__source: ListenersT}; // do not use __source, just here to allow EventTypes to work
}
type EventTypes<T> = T extends EventEmitter<infer U> ? U : never;
/////////////////////////////////////////////////////////////////////////////////////////////
interface VehicleEvents {
accelerate(acceleration: number): void;
brake(deceleration: number): void;
}
interface BusEvents extends VehicleEvents {
doorStateChange(front: boolean, middle: boolean, rear: boolean): void
}
interface Vehicle extends EventEmitter<VehicleEvents> {}
class Vehicle {
public constructor() {
this.on('brake', () => this.flashBrakeLights()); //ok
}
public flashBrakeLights(): void { }
public hitTheGas(strength: number): void { this.emit('accelerate', strength * 42); } // ok
}
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
interface Bus extends EventEmitter<BusEvents> { }
function extendEmitter<TBaseCtor extends new (...a: any[])=> any>(ctor: TBaseCtor){
return function<TEvents extends EventTypes<InstanceType<TBaseCtor>>>(){
return ctor as (new (...a: ConstructorParameters<TBaseCtor>) => Omit<InstanceType<TBaseCtor>, 'on' | 'emit'> & EventEmitter<TEvents>)
}
}
class Bus extends extendEmitter(Vehicle)<BusEvents>() {
public doorState: [boolean, boolean, boolean] = [false, false, false];
public constructor() {
super();
this.on('accelerate', () => {
this.door(0, false);
this.door(1, false);
this.door(2, false);
});
}
public door(index: number, state: boolean): void {
this.doorState[index] = state;
this.emit('doorStateChange', ...this.doorState);
}
}
export const bus = new Bus();
