TypeSafe Event Emitter: Type-Safe Pub/Sub with Middleware for TypeScript
TypeScript··52 views
/**
* A strongly-typed event emitter that provides full TypeScript type safety for events and their payloads.
* Unlike traditional event emitters, this implementation ensures:
* - Event names and payloads are type-checked at compile time
* - Handlers receive correctly typed event data
* - Events can only be emitted with the correct payload type
* - Support for async event handlers
* - Proper memory management with automatic cleanup
* - Support for one-time event listeners
* - Middleware support for event interception
*
* Example usage:
* ```typescript
* // Define your events
* type Events = {
* userJoined: { userId: string; timestamp: number };
* messageReceived: { text: string; from: string };
* typing: undefined; // Events without payload
* };
*
* // Create emitter
* const events = new TypedEventEmitter<Events>();
*
* // Type-safe event handling
* events.on('userJoined', ({ userId, timestamp }) => {
* console.log(`User ${userId} joined at ${timestamp}`);
* });
*
* // Won't compile if payload doesn't match type
* events.emit('userJoined', { userId: '123', timestamp: Date.now() }); // ✅
* events.emit('userJoined', { userId: '123' }); // ❌ Type error
* ```
*/
type EventMap = Record<string | symbol, any>;
type EventKey<T extends EventMap> = string & keyof T;
type EventReceiver<T> = (params: T) => void | Promise<void>;
interface Middleware<T extends EventMap> {
pre?: <K extends EventKey<T>>(eventName: K, data: T[K]) => T[K] | false | Promise<T[K] | false>;
post?: <K extends EventKey<T>>(eventName: K, data: T[K]) => void | Promise<void>;
}
export class TypedEventEmitter<T extends EventMap> {
private events = new Map<
EventKey<T>,
Set<{
handler: EventReceiver<any>;
once: boolean;
}>
>();
private middlewares: Middleware<T>[] = [];
/**
* Add a middleware to intercept events
*/
use(middleware: Middleware<T>) {
this.middlewares.push(middleware);
return this;
}
/**
* Register an event handler
*/
on<K extends EventKey<T>>(eventName: K, handler: EventReceiver<T[K]>) {
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set());
}
this.events.get(eventName)!.add({ handler, once: false });
return () => this.off(eventName, handler);
}
/**
* Register a one-time event handler
*/
once<K extends EventKey<T>>(eventName: K, handler: EventReceiver<T[K]>) {
if (!this.events.has(eventName)) {
this.events.set(eventName, new Set());
}
this.events.get(eventName)!.add({ handler, once: true });
return () => this.off(eventName, handler);
}
/**
* Remove an event handler
*/
off<K extends EventKey<T>>(eventName: K, handler: EventReceiver<T[K]>) {
const handlers = this.events.get(eventName);
if (handlers) {
for (const handlerObj of handlers) {
if (handlerObj.handler === handler) {
handlers.delete(handlerObj);
break;
}
}
if (handlers.size === 0) {
this.events.delete(eventName);
}
}
}
/**
* Remove all event handlers for a given event
*/
removeAllListeners<K extends EventKey<T>>(eventName?: K) {
if (eventName) {
this.events.delete(eventName);
} else {
this.events.clear();
}
}
/**
* Emit an event with type-safe payload
*/
async emit<K extends EventKey<T>>(eventName: K, data: T[K]) {
const handlers = this.events.get(eventName);
if (!handlers) return;
// Run pre-middleware
for (const middleware of this.middlewares) {
if (middleware.pre) {
const result = await middleware.pre(eventName, data);
if (result === false) return;
data = result;
}
}
// Execute handlers
const promises: Promise<void>[] = [];
const handlersToRemove = new Set<{ handler: EventReceiver<T[K]>; once: boolean }>();
for (const handlerObj of handlers) {
if (handlerObj.once) {
handlersToRemove.add(handlerObj);
}
const result = handlerObj.handler(data);
if (result instanceof Promise) {
promises.push(result);
}
}
// Remove one-time handlers
for (const handlerObj of handlersToRemove) {
this.off(eventName, handlerObj.handler);
}
// Wait for all async handlers
await Promise.all(promises);
// Run post-middleware
for (const middleware of this.middlewares) {
if (middleware.post) {
await middleware.post(eventName, data);
}
}
}
/**
* Get the number of listeners for a given event
*/
listenerCount<K extends EventKey<T>>(eventName: K): number {
const handlers = this.events.get(eventName);
return handlers ? handlers.size : 0;
}
/**
* Check if an event has any listeners
*/
hasListeners<K extends EventKey<T>>(eventName: K): boolean {
return this.listenerCount(eventName) > 0;
}
}