andywer / typed-emitter

🔩 Type-safe event emitter interface for TypeScript
MIT License
268 stars 24 forks source link

[QUESTION] How can I use in an abstract class that extends `EventEmitter`? #30

Open JakeElder opened 2 years ago

JakeElder commented 2 years ago

Thank you for the package! I'm struggling to achieve something that I'm sure is an easy task for some. Is this possible?

How can I extend an abstract EventEmitter class, with common events?

Example:

import EventEmitter from "events";
import TypedEmitter from "typed-emitter";

type BaseEvents = {
  BASE_EVENT: (arg: string) => void;
};

abstract class BaseEmitter<T> extends (EventEmitter as {
  new <T>(): TypedEmitter<T>;
})<T> {
  constructor() {
    super();
    // How can I make this type safe ????
    this.emit("BASE_EVENT", "string-value"); // <-- How can I achieve type safety here
  }
}

type ImpOneEvents = {
  C1_EVENT: (a: number) => void;
};
class ImpOne extends BaseEmitter<ImpOneEvents> {
  constructor() {
    super();
    this.emit("C1_EVENT", 1); // OK: type checks ok (GOOD)
    this.emit("C1_EVENT", "bla"); // ERROR: string not assignable (GOOD)
  }
}

type ImpTwoEvents = {
  C2_EVENT: (a: boolean) => void;
};
class ImpTwo extends BaseEmitter<ImpTwoEvents> {
  constructor() {
    super();
    this.emit("C2_EVENT", true); // OK: type checks ok (GOOD)
  }
}

const impTwo = new ImpTwo();
impTwo.on("C2_EVENT", (a) => {
  parseFloat(a); // ERROR: Type of boolean not assignable to parameter of type string (GOOD)
});

I've tried

type BaseEvents = {
  BASE_EVENT: (arg: string) => void;
};

abstract class BaseEmitter<T = {}> extends (EventEmitter as {
  new <T>(): TypedEmitter<T>;
})<T & BaseEvents> {
...

And lots of other things. The above results in

Argument of type '[string]' is not assignable to parameter of type 'Arguments<(T & BaseEvents)["BASE_EVENT"]>'.

For line

this.emit("BASE_EVENT", "string-value"); // <-- How can I achieve type safety here
andywer commented 2 years ago

Hmm, I'm not sure why you would extend the EventEmitter, but generally speaking… I think this might be the issue (didn't test it, though):

  abstract class BaseEmitter<T = {}> extends (EventEmitter as {
-   new <T>(): TypedEmitter<T>;
+   new <T>(): TypedEmitter<T & BaseEvents>;
  })<T & BaseEvents> {
JakeElder commented 2 years ago

Thank you for the reply @andywer - this results in a similar error as in my post. If I'm correct, that would make TypedEmitter<T & BaseEvents & BaseEvents> as the final generic argument <T & BaseEvents>

I found this works, with sometimes clunky (but correct) error messages.

export abstract class DiscordBot<T = {}> extends (EventEmitter as {
  new <T>(): TypedEmitter<BaseEvents> & TypedEmitter<T>;
})<T> {

Though I'm not sure my software design is correct either.

The code I am writing is an abstract class for Discord bots. Each of the bots should emit events like 'READY' once initialisation has completed, and I would like to be able to emit common events from methods in the abstract class, 'READY', 'COMMAND_RECEIVED', etc.

The concrete bot implementations should each be able to emit their own events too, 'BOT_SPECFIC_TASK_COMPLETE' etc.

sp00x commented 2 years ago

This was easy with version 1.x of this library, where you used interfaces instead of types, and then you could easily just create generic with type constraints that said <TEvents extends IBaseEvents>

andywer commented 2 years ago

@sp00x I don't quite understand. The TypedEmitter is still an interface, just that the BaseEvents are now a type alias, but that's just used as a type constraint. <TEvents extends IBaseEvents> should work just fine, not?

andywer commented 2 years ago

Sorry, just saw #32.