Typed Event Emitter With Listener Limits
A generic event emitter whose event names and payload types are checked at compile time, with a listener cap that prevents unbounded growth.
Code
export type EventMap = Record<string, unknown>;
export class TypedEmitter<TEvents extends EventMap> {
private readonly listeners = new Map<keyof TEvents, Set<(payload: never) => void>>();
private readonly maxListeners: number;
constructor(options: { maxListeners?: number } = {}) {
this.maxListeners = options.maxListeners ?? 10;
if (!Number.isInteger(this.maxListeners) || this.maxListeners < 1) {
throw new RangeError("maxListeners must be a positive integer");
}
}
on<TKey extends keyof TEvents>(
event: TKey,
listener: (payload: TEvents[TKey]) => void,
): () => void {
let set = this.listeners.get(event);
if (!set) {
set = new Set();
this.listeners.set(event, set);
}
if (set.size >= this.maxListeners) {
throw new Error(`listener limit ${this.maxListeners} exceeded for event ${String(event)}`);
}
const wrapped = listener as (payload: never) => void;
set.add(wrapped);
return () => this.off(event, listener);
}
off<TKey extends keyof TEvents>(
event: TKey,
listener: (payload: TEvents[TKey]) => void,
): void {
const set = this.listeners.get(event);
if (!set) return;
set.delete(listener as (payload: never) => void);
if (set.size === 0) this.listeners.delete(event);
}
emit<TKey extends keyof TEvents>(event: TKey, payload: TEvents[TKey]): void {
const set = this.listeners.get(event);
if (!set) return;
// snapshot so removal during dispatch does not skip listeners
for (const listener of [...set]) listener(payload as never);
}
listenerCount(event: keyof TEvents): number {
return this.listeners.get(event)?.size ?? 0;
}
}Report a problem with this exampleOpens a pre-filled issue on GitHub.