Home > database >  Creating an extendible class that implements an extendible event emitter
Creating an extendible class that implements an extendible event emitter

Time:07-19

I'm trying to create a series of extendible classes that, at each level, can add additional event handlers to an "EventMap" structure that is used as to type check an emit() method.

class Level1_Extendible (Events: 'deliver1')
 |  
 | 
 |-> class Level2_Extendible (Events: 'deliver1' | 'deliver2')
      |  
      |-> class Level3_Final (Events: 'deliver1' | 'deliver2' | 'deliver3')

This is very much an EventEmitter pattern, but extendible over multiple levels of base classes. The idea seems pretty simple in my head using generics with constraints etc. It all works fine for the final non-extendible class. But the base classes do not allow access to the events that they should already know about:

  • Level1_Extendible can't emit 'deliver1'
  • Level2_Extendible can't emit 'deliver2' or 'deliver1'
  • Level3_Final, however, can emit 'deliver3', 'deliver2' and 'deliver1'
interface EventMap1 {
    deliver1(): void;
}

class Level1_Extendable<EM extends EventMap1 = EventMap1> {
    emit<Key extends keyof EM>(key: Key, ...args: EventEmitterParameters<EM[Key]>): void {
        // Emit the event...
    }

    doSomething() {
        // This statement errors
        this.emit('deliver1');
    }
}

interface EventMap2 extends EventMap1 {
    deliver2(): void;
}

class Level2_Extendable<EM extends EventMap2 = EventMap2> extends Level1_Extendable<EM> {
    doSomething() {
        // Both of these statements error
        this.emit('deliver1');
        this.emit('deliver2');
    }
}

interface EventMap3 extends EventMap2 {
    deliver3(payload: boolean): void;
}

class Level3_Final extends Level2_Extendable<EventMap3> {
    doSomething() {
        // These statements are fine!
        this.emit('deliver1');
        this.emit('deliver2');
        this.emit('deliver3', true);
    }
}

Playground Link.

It seems the unresolved generics get in the way, even though the base classes should know about the properties that they add to the mix based on the generic constraints. Anyone have any idea what I'm doing wrong here, another way this can be achieved or if what I'm trying to do is not possible currently? Thanks!

CodePudding user response:

The problem here seems to be that the compiler defers evaluation of EventEmitterParameters<EM["deliver1"]> inside the implementation of Level1_Extendable<EM>'s emit(), since it is a conditional type that depends upon the as-yet unspecified generic type parameter EM extends EventMap1. Instead of spending a lot of processing time probing generic conditional types for possible non-generic subtypes and supertypes, which would often be wasted, the compiler essentially gives up. It doesn't know what EventEmitterParameters<EM["deliver1"]> is going to be, so it treats it as an opaque type to which no value of almost any other type is seen as assignable. This is a general limitation of generic conditional types in TypeScript. See microsoft/TypeScript#43638 for more information, and microsoft/TypeScript#38685 for a related feature request.

If EM were specified as EventMap1 or EventMap2 or EventMap3, then the problem goes away. You see this inside Level3_Final. If you want similar behavior inside Level1_Extendable or Level2_Extendable, you might need to upcast this from the polymorphic this type to a specific type of the current class. There are different ways to do this; you prefer specifying the this parameter for the doSomething() method implementation, which looks like this:

doSomething(this: Level1_Extendable) {
    this.emit('deliver1'); // okay
}

and:

doSomething(this: Level2_Extendable) {
    this.emit('deliver1'); // okay
    this.emit('deliver2'); // okay
}

Playground link to code

  • Related