Search code examples
typescript-genericseventemitter

Restrict emits from EventEmitter3


Related to the accepted answer: https://stackoverflow.com/a/63639280/17928771

EventEmitter3 is a generic class that takes an (as one example) Interface of Events/Handlers. I'm trying to restrict IRaceManager emits to IRaceEvents. I have tried:

Attempt 1.

class POSRaceManager implements IRaceManager extends EventEmitter<IRaceEvents> {}

let raceManager: IRaceManager = new POSRaceManager();
raceManager.emit('moo'); // error, no `emit` on IRaceManager

Attempt 2.

interface IRaceManager extends EventEmitter<IRaceEvents> {} // TS2749: 'EventEmitter' refers to a value, but is being used as a type here. Did you mean 'typeof EventEmitter'?

The following attempt works, but doesn't limit the emit or on to the IRaceEvents (or an extension of IRaceEvents)

Attempt 3.

This fails:

type RaceEventEmitterType<T extends IRaceEvents> = InstanceType<typeof EventEmitter>;
type IRaceManager<T extends IRaceEvents> = RaceEventEmitterType<T>;

let raceManager: IRaceManager = new POSRaceManager<IRaceEvents>;
raceManager.emit("moo"); // no error because of `InstanceType<typeof EventEmitter>`

Final attempt.

type RaceEventEmitterType<T extends IRaceEvents> = InstanceType<typeof EventEmitter<IRaceEvents>>; // Need to investigate what this does, actually. I overlooked a syntax error before.

Any ideas to restrict emissions from raceManager<?> to T extends IRaceEvents only?

Edit:

I am currently settled on the following (which works, I am just wondering if there is a way to resolve as above):

type RaceEventEmitter<T extends IRaceEvents> = InstanceType<typeof EventEmitter>;
type IRaceManager<T extends IRaceEvents> = RaceEventEmitter<T>;
class POSRaceManager extends EventEmitter<IRaceEvents> implements IRaceManager<IRaceEvents>;

Solution

  • "What part don't you understand?"

    Answer: Core concept? (Archer quote)

    This has been a rough learning curve but finally have a solution to move forward:

    type TRaceEvents = {
        raceUpdate: (race: IRace | null) => void;
    }
    
    //=> Here, Extending EventEmitter is essential to ensure the implementation properly
    //  extends `EventEmitter`; however, `extends EventEmitter<TRaceEvents>` has
    //  no additional affect (though is preferred as the Runner could fail to 
    //  function properly if the impl isn't Typed with `TRaceEvents`)
    interface IRaceManager extends EventEmitter {
      race: IRace;
      start() => void;
    }
    
    class StandardRaceManager extends EventEmitter<TRaceEvents> implements IRaceManager {
      constructor(public race: IRace) { super(); }
      start() {
        this.race.status = "Running";
    
        //=> Here, `this.race` red squiggled. Why?
        // this.emit("raceUpdate", this.race);
    
        //=> Here, no red squiggly, but casting to `any` is no bueno?
        this.emit("raceUpdate", this.race as any);
    }
    
    const standardRace: IRaceManager = new StandardRaceManager({_id: 1, status: "taxiing"} as IRace);
    
    //=> Here, race is properly typed with `IRace | null`
    standardRace.on("raceUpdate", race => console.log(race));
    
    standardRace.start();
    
    //=> No red squiggly. Why?
    standardRace.emit("raceUpdate", race);
    

    Perhaps I've been fighting (and continue to fight with) with an IDE error this whole time.