Skip to content

Instantly share code, notes, and snippets.

@randomchars42
Last active February 2, 2021 22:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save randomchars42/ac397c14c3d9fc83b668b21a95a3047f to your computer and use it in GitHub Desktop.
Save randomchars42/ac397c14c3d9fc83b668b21a95a3047f to your computer and use it in GitHub Desktop.
TypeScript: A fully typed EventEmitter that can emit multiple events in a type-safe way.
// built upon
// https://codereview.stackexchange.com/a/215317/236617
// https://basarat.gitbook.io/typescript/main-1/typed-event
// https://stackoverflow.com/a/66003746/14979776
interface Disposable {
dispose(): void;
}
// used to check if parameters for .emit() are valid parameters for the event
// `Symbol` is needed to tell the compiler the resulting type can be `spread`
type EventTypes<T> = Symbol & T extends (...args: infer U) => any ? U: never;
// create a map:
// EventMap = { "ListenerType1": Function[], ..., "ListenerTypeN": Function[] }
type ListenerRecord<T> = {[K in keyof T]: T[keyof T]};
type EventMap<T extends ListenerRecord<T>> = {[K in keyof T]: T[K][]};
class EventEmitter<T extends ListenerRecord<T>> {
protected _listeners: Partial<EventMap<T>> = {};
protected _single_use_listeners: Partial<EventMap<T>> = {};
protected _get_listeners<K extends keyof T>(event_name: K): T[K][] {
if (!this._listeners[event_name]) {
this._listeners[event_name] = [];
}
return this._listeners[event_name] as T[K][];
}
protected _get_single_use_listeners<K extends keyof T>(event_name: K): T[K][] {
if (!this._single_use_listeners[event_name]) {
this._single_use_listeners[event_name] = [];
}
return this._single_use_listeners[event_name] as T[K][];
}
on<K extends keyof T>(event_name: K, listener: T[K]): Disposable {
this._get_listeners(event_name).push(listener);
return {
dispose: () => {
this.off(event_name, listener);
}
}
}
once<K extends keyof T>(event_name: K, listener: T[K]): void {
this._get_single_use_listeners(event_name).push(listener);
}
off<K extends keyof T>(event_name: K, listener: T[K]): void {
const listeners = this._get_listeners(event_name);
const index = listeners.indexOf(listener);
if (index !== -1) {
listeners.splice(index, 1);
}
}
emit<K extends keyof T>(event_name: K, ...param_list: EventTypes<T[K]>): void {
this._get_listeners(event_name).forEach((listener) => listener(...param_list));
if (this._get_single_use_listeners(event_name).length > 0) {
const callstack = this._get_single_use_listeners(event_name);
callstack.forEach((listener) => listener(...param_list));
this._get_single_use_listeners(event_name).splice(0);
}
}
}
// example implementation:
// events "congratulate" and "clap_hands" expect different types (/ numbers) of parameters
interface HappyEvents {
congratulate(event: string): void,
clap_hands(event: number): void,
}
class HappyEmitter extends EventEmitter<HappyEvents> {
// your additional functions go here
}
const happy_emitter = new HappyEmitter();
// or (if you don't need additional methods)
const happy_emitter2 = new EventEmitter<HappyEvents>();
// register events
happy_emitter.on("congratulate", (param) => console.log(param + "Yay!"));
happy_emitter.once("congratulate", () => console.log("Lalala"));
happy_emitter.on("clap_hands", () => console.log("clap"));
// do not attempt:
happy_emitter.on("congratulate", (param: number) => console.log(param));
// throws: Argument of type '(param: number) => void' is not assignable to parameter of type '(event: string) => void'.
happy_emitter.on("clap_hands", (param: number, param2: string) => console.log(param));
// throws: Argument of type '(param: number, param2: number) => void' is not assignable to parameter of type '(event: string) => void'.
// fire events
happy_emitter.emit("congratulate", "Yippey ya ");
happy_emitter.emit("congratulate", "Hooray! ");
happy_emitter.emit("clap_hands", 2);
happy_emitter.emit("clap_hands", 1);
// do not attempt:
happy_emitter.emit("congratulate", 2);
// throws: Type 'number' is not assignable to type 'string'
happy_emitter.emit("clap_hands", "r");
// throws: Type 'string' is not assignable to type 'number'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment