Skip to content

Instantly share code, notes, and snippets.

@safareli
Last active December 29, 2020 06:49
Show Gist options
  • Save safareli/014fb65903d796ac57779de651eb8b06 to your computer and use it in GitHub Desktop.
Save safareli/014fb65903d796ac57779de651eb8b06 to your computer and use it in GitHub Desktop.
typed TypeScript event emitter
type Listener<T> = [T] extends [void] ? () => void : (payload: T) => void;
type BaseSpec = Record<string, unknown>;
type ListenerMap<T extends BaseSpec> = { [K in keyof T]?: Listener<T[K]>[] };
type EventPayload<T> = [T] extends [void] ? [] : [payload: T];
/** Typed event emitter implementation
* example:
* ```ts
* const myEventEmitter = new EventEmitter<{
* foo: string[];
* baz: void;
* }>();
*
*
* myEventEmitter.emit("baz")
* myEventEmitter.on("foo", ________ )
* // on(key: "foo", listener: Listener<string[]>): () => void
*
* myEventEmitter.emit("foo", _____ )
* // emit(key: "foo", event: string[]): void
*
* ```
*/
export class EventEmitter<T extends BaseSpec> {
private listeners: ListenerMap<T> = {};
/**
* Appends an event `listener` for events associated with `eventKey`, returns
* cleanup function which removes the listener.
*/
on<K extends Extract<keyof T, string>>(
eventKey: K,
listener: Listener<T[K]>
): () => void {
const listeners = this.listeners[eventKey];
if (listeners === undefined) {
this.listeners[eventKey] = [listener];
} else {
listeners.push(listener);
}
return () => this.off(eventKey, listener);
}
/**
* Remove the event `listener` attached for events associated with `eventKey`
*/
off<K extends Extract<keyof T, string>>(
eventKey: K,
listener: Listener<T[K]>
) {
const listeners = this.listeners[eventKey];
if (listeners === undefined) return;
const callbackIndex = listeners.indexOf(listener);
if (callbackIndex > -1) listeners.splice(callbackIndex, 1);
}
/**
* Emit `event` associated with `eventKey`
*/
emit<K extends Extract<keyof T, string>>(
eventKey: K,
...args: EventPayload<T[K]>
) {
this.listeners[eventKey]?.forEach((listener) =>
listener(
// Ideally we wouldn't need this cast. Related issue:
// https://github.com/microsoft/TypeScript/issues/42139
args[0] as T[K]
)
);
}
}
// Listener<T>
export class EventEmitter2<T> {
private multiEventEmitter: EventEmitter<{ event: T }> = new EventEmitter();
/**
* Appends an event `listener` for events associated with `eventKey`, returns
* cleanup function which removes the listener.
*/
on(listener: Listener<T>): () => void {
return this.multiEventEmitter.on("event", listener);
}
/**
* Remove the event `listener` attached for events associated with `eventKey`
*/
off(listener: Listener<T>) {
this.multiEventEmitter.off("event", listener);
}
/**
* Emit `event` associated with `eventKey`
*/
emit(...args: EventPayload<T>) {
this.multiEventEmitter.emit("event", ...args);
}
}
new EventEmitter2<void>().emit();
new EventEmitter2<undefined>().emit();
new EventEmitter2<null>().emit(null);
new EventEmitter2<boolean>().on((payload: boolean) => {
payload;
});
new EventEmitter2<boolean>().emit(true);
export class Emitter<T> {
private emitter = new Event.EventEmitter();
/**
* Appends an event `listener` for emitted events, returns
* cleanup function which removes the listener.
*/
on(listener: (event: T) => void): () => void {
this.emitter.on("event", listener);
return () => this.off(listener);
}
/**
* Remove the already attached event `listener`.
*/
off(listener: (event: T) => void) {
this.emitter.off("event", listener);
}
/**
* Emit the `event`
*/
emit(event: T) {
this.emitter.emit("event", event);
}
}
export class EventEmitter<T extends Record<string, unknown>> {
private emitter = new Event.EventEmitter();
/**
* Appends an event `listener` for events associated with `eventKey`,
* returns cleanup function which removes the listener.
*/
on<K extends Extract<keyof T, string>>(
eventKey: K,
listener: (event: T[K]) => void
): () => void {
this.emitter.on(eventKey, listener);
return () => this.off(eventKey, listener);
}
/**
* Remove the event `listener` attached for events associated with `eventKey`
*/
off<K extends Extract<keyof T, string>>(
eventKey: K,
listener: (event: T[K]) => void
) {
this.emitter.off(eventKey, listener);
}
/**
* Emit `event` associated with `eventKey`
*/
emit<K extends Extract<keyof T, string>>(eventKey: K, event: T[K]) {
this.emitter.emit(eventKey, event);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment