A generic event map interface lets an emitter's `on`/`emit` methods be fully type-checked per event name, instead of accepting arbitrary strings and `any` payloads.
ts
interface WalletEvents {
connect: { address: string };
disconnect: { reason: string };
balanceChange: { address: string; newBalance: bigint };
}
class TypedEmitter<Events extends Record<string, unknown>> {
private listeners: { [K in keyof Events]?: ((payload: Events[K]) => void)[] } = {};
on<K extends keyof Events>(event: K, cb: (payload: Events[K]) => void) {
(this.listeners[event] ??= []).push(cb);
}
emit<K extends keyof Events>(event: K, payload: Events[K]) {
this.listeners[event]?.forEach(cb => cb(payload));
}
}
const emitter = new TypedEmitter<WalletEvents>();
emitter.on("connect", (p) => console.log(p.address)); // p: { address: string }
emitter.emit("connect", { address: "0xabc" });
// emitter.emit("connect", { reason: "x" }); // error: wrong payload shapeThis pattern combines generics, mapped types, and `keyof` to give compile-time guarantees that every emitted event carries the right payload shape and every listener receives exactly that shape.