export type EventMap = Record<string, unknown[]>;
type Listener<Args extends unknown[]> = (...args: Args) => void;
export class TypedEmitter<Events extends EventMap> {
private listeners = new Map<keyof Events, Set<Listener<any>>>();
on<K extends keyof Events>(event: K, fn: Listener<Events[K]>): () => void {
let set = this.listeners.get(event);
if (!set) {
set = new Set();
this.listeners.set(event, set);
}
set.add(fn);
return () => this.off(event, fn);
}
off<K extends keyof Events>(event: K, fn: Listener<Events[K]>): void {
this.listeners.get(event)?.delete(fn);
}
once<K extends keyof Events>(event: K, fn: Listener<Events[K]>): () => void {
const wrapper: Listener<Events[K]> = (...args) => {
this.off(event, wrapper);
fn(...args);
};
return this.on(event, wrapper);
}
emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
const set = this.listeners.get(event);
if (!set) return;
// Copy so handlers may unsubscribe during dispatch safely.
for (const fn of [...set]) {
fn(...args);
}
}
removeAll<K extends keyof Events>(event?: K): void {
if (event === undefined) this.listeners.clear();
else this.listeners.delete(event);
}
}
import { TypedEmitter, EventMap } from './TypedEmitter';
export interface User {
id: string;
name: string;
}
export interface AppEvents extends EventMap {
login: [user: User];
logout: [];
'message:received': [channelId: string, body: string];
error: [error: Error];
}
export const appBus = new TypedEmitter<AppEvents>();
// Producers get full checking on payload shape.
export function signIn(user: User): void {
appBus.emit('login', user);
}
export function reportError(err: unknown): void {
appBus.emit('error', err instanceof Error ? err : new Error(String(err)));
}
// Consumers infer exact argument types.
appBus.on('message:received', (channelId, body) => {
console.log(`[${channelId}] ${body}`);
});
import { useEffect, useRef } from 'react';
import { TypedEmitter, EventMap } from './TypedEmitter';
type Handler<E extends EventMap, K extends keyof E> = (...args: E[K]) => void;
export function useTypedEvent<E extends EventMap, K extends keyof E>(
emitter: TypedEmitter<E>,
event: K,
handler: Handler<E, K>,
): void {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const listener = ((...args: E[K]) => {
savedHandler.current(...args);
}) as Handler<E, K>;
const unsubscribe = emitter.on(event, listener);
return unsubscribe;
}, [emitter, event]);
}
A plain EventEmitter from Node accepts any string as an event name and passes listeners any arguments, which discards the exact information a type system is good at tracking. This snippet builds a small emitter where a single event map drives everything: the set of valid event names, and the precise payload shape each listener receives.
In TypedEmitter, the class is parameterized by Events extends EventMap, where EventMap is just a record from event names to argument tuples. Using tuple types like [user: User] lets each event carry a fully-typed, multi-argument payload while keeping emit variadic. The listeners are stored in a Map<keyof Events, Set<Listener>>, and every public method — on, off, once, and emit — is generic over K extends keyof Events. That single constraint is what makes on('login', ...) know its callback receives a User, and makes emit('login', 42) a compile error. The Set is chosen so duplicate registrations of the same reference are ignored and removal is O(1).
The once method wraps the user callback in a self-removing shim; note it stores the wrapper, not the original, so off still works because once returns the same unsubscribe idiom via on. emit copies the listener set before iterating so a handler that unsubscribes mid-dispatch doesn't mutate the collection being walked — a classic re-entrancy pitfall in observer implementations.
The AppEvents interface in the second tab shows the real payoff: the event contract lives in one place, and both producers and consumers share it. Adding an event or changing a payload immediately surfaces every mismatched call site.
Finally, useTypedEvent hook adapts the emitter to React. It takes the emitter, an event key, and a handler, then subscribes inside useEffect and returns the cleanup function so the listener is removed on unmount or when dependencies change. A useRef holds the latest handler so a fresh closure each render doesn't force a resubscribe, avoiding stale-closure bugs while keeping the effect dependency list minimal. Together these files show how one typed contract can flow from the emitter core through domain events into idiomatic component code.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
import React from "react";
type FallbackProps = {
error: Error;
reset: () => void;
};
React Error Boundary + error reporting hook
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
openAnalyzer: true,
});
/** @type {import('next').NextConfig} */
Next.js bundle analyzer for targeted performance work
// Basic event listener
const button = document.getElementById('myButton');
button.addEventListener('click', function(event) {
console.log('Button clicked!');
console.log('Event type:', event.type);
Event handling and event delegation patterns in JavaScript
Share this code
Here's the card — post it anywhere.