Skip to content

Instantly share code, notes, and snippets.

@frzi
Last active February 9, 2021 21:06
Show Gist options
  • Save frzi/400c7dc74e37a60d21bc5c3b73c5c739 to your computer and use it in GitHub Desktop.
Save frzi/400c7dc74e37a60d21bc5c3b73c5c739 to your computer and use it in GitHub Desktop.
Strongly typed Event Emitter
/**
* Custom written EventEmitter.
* Shares most of the same interface as Node.js' Event Emitter.
* Except everything is strongly typed and stored in `#listeners`.
*/
type Arguments<T> = [T] extends [(...args: infer U) => any] ? U : [T] extends [void] ? [] : [T]
type Listener<T> = (...argv: Arguments<T>) => void
interface DefaultEvents {
[key: string]: (...values: unknown[]) => void
}
class EventDescription<T extends () => void> {
constructor(
public readonly fn: T,
public readonly once: boolean
) {}
}
export default class EventEmitter<Events = DefaultEvents> {
#listeners: {
[K in keyof Events]?: EventDescription<Listener<Events[K]>>[]
} = {}
//
// Add / remove / emit.
private addEvent<K extends keyof Events>(event: K, fn: Listener<Events[K]>, once: boolean): this {
this.#listeners[event] = this.#listeners[event] || []
this.#listeners[event].push(new EventDescription(fn, once))
return this
}
public on<K extends keyof Events>(event: K, fn: Listener<Events[K]>): this {
return this.addEvent(event, fn, false)
}
public once<K extends keyof Events>(event: K, fn: Listener<Events[K]>): this {
return this.addEvent(event, fn, true)
}
public off<K extends keyof Events>(event: K, fn: Listener<Events[K]>): this {
if (this.#listeners[event]) {
this.#listeners[event] = this.#listeners[event].filter(description => description.fn != fn)
if (this.#listeners[event].length == 0) {
delete this.#listeners[event]
}
}
return this
}
public removeAllListeners<K extends keyof Events>(event?: K): this {
if (event) {
delete this.#listeners[event]
}
else {
this.#listeners = {}
}
return this
}
public emit<K extends keyof Events>(event: K, ...argv: Arguments<Events[K]>): this {
if (this.#listeners[event]) {
this.#listeners[event] = this.#listeners[event].filter(description => {
description.fn.call(this, ...argv)
return !description.once
})
}
return this
}
//
// Query etc.
public listeners<K extends keyof Events>(event: K): Listener<Events[K]>[] {
return this.#listeners[event]?.map(description => description.fn) || []
}
public listenersCount<K extends keyof Events>(event: K): number {
return this.#listeners[event]?.length ?? 0
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment