Generic Event Emitter Class
Build a reusable typed event emitter from scratch.
Generic Event Emitter Class is a free TypeScript Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the TypeScript Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Goal: A TypedEmitter Class
We will build a reusable TypedEmitter<E> class generic over an event map E, with fully typed on, off, and emit methods.
interface Events { login: { userId: string }; logout: void }
// class TypedEmitter<Events> will manage handlers safely.
console.log("building a typed emitter");A Handler Type Alias
Define a handler type parameterized by the event name so each listener receives the right payload.
type Handler<E, K extends keyof E> = (payload: E[K]) => void;
// E[K] is the payload type for event K in map E.
console.log("handler typed by E[K]");The listeners Map
Store handlers keyed by event name. A mapped type gives each key an array of correctly typed handlers.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
}
console.log("listeners map ready");Implementing on
The on method appends a handler to the array for its event, creating the array on first use.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(event: K, handler: (p: E[K]) => void): void {
(this.listeners[event] ||= []).push(handler);
}
}
console.log("on implemented");Implementing emit
The emit method looks up handlers for the event and calls each with the typed payload.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(e: K, h: (p: E[K]) => void) { (this.listeners[e] ||= []).push(h); }
emit<K extends keyof E>(e: K, payload: E[K]): void {
(this.listeners[e] || []).forEach(h => h(payload));
}
}
console.log("emit implemented");Implementing off
The off method removes a previously registered handler by filtering it out of the array.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(e: K, h: (p: E[K]) => void) { (this.listeners[e] ||= []).push(h); }
off<K extends keyof E>(e: K, h: (p: E[K]) => void) {
this.listeners[e] = (this.listeners[e] || []).filter(x => x !== h);
}
}
console.log("off implemented");Putting the Class Together
Here is the full class with all three methods, generic over any event map you supply.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(e: K, h: (p: E[K]) => void) { (this.listeners[e] ||= []).push(h); }
off<K extends keyof E>(e: K, h: (p: E[K]) => void) { this.listeners[e] = (this.listeners[e] || []).filter(x => x !== h); }
emit<K extends keyof E>(e: K, p: E[K]) { (this.listeners[e] || []).forEach(h => h(p)); }
}
console.log("TypedEmitter complete");Using TypedEmitter
Instantiate with a concrete event map. Now on and emit are fully typed for those events.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(e: K, h: (p: E[K]) => void) { (this.listeners[e] ||= []).push(h); }
emit<K extends keyof E>(e: K, p: E[K]) { (this.listeners[e] || []).forEach(h => h(p)); }
}
interface Events { login: { userId: string } }
const bus = new TypedEmitter<Events>();
bus.on("login", p => console.log("hi", p.userId));
bus.emit("login", { userId: "u1" });Type Errors Are Caught
The class rejects unknown events and wrong payloads at compile time, just like the standalone functions did.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(e: K, h: (p: E[K]) => void) { (this.listeners[e] ||= []).push(h); }
emit<K extends keyof E>(e: K, p: E[K]) { (this.listeners[e] || []).forEach(h => h(p)); }
}
interface Events { ping: { at: number } }
const bus = new TypedEmitter<Events>();
// bus.emit("pong", { at: 1 }); // Error
bus.emit("ping", { at: 1 });
console.log("typed bus");Multiple Handlers Per Event
Because each event maps to an array, you can register many handlers and they all fire on emit.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(e: K, h: (p: E[K]) => void) { (this.listeners[e] ||= []).push(h); }
emit<K extends keyof E>(e: K, p: E[K]) { (this.listeners[e] || []).forEach(h => h(p)); }
}
interface Events { tick: { n: number } }
const bus = new TypedEmitter<Events>();
bus.on("tick", p => console.log("a", p.n));
bus.on("tick", p => console.log("b", p.n));
bus.emit("tick", { n: 7 });A Foundation You Can Extend
This class is a solid base. Next we will refine listener-argument inference and add once semantics.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(e: K, h: (p: E[K]) => void) { (this.listeners[e] ||= []).push(h); }
emit<K extends keyof E>(e: K, p: E[K]) { (this.listeners[e] || []).forEach(h => h(p)); }
}
const bus = new TypedEmitter<{ go: void }>();
bus.on("go", () => console.log("go!"));
bus.emit("go", undefined as void);Quick Check: Generic Emitter Class
Test your understanding of the generic event emitter class.
Recap: Generic Event Emitter Class
You built TypedEmitter<E> with a mapped listeners store and fully typed on, off, and emit methods, giving a reusable, type-safe pub/sub for any event map.
class TypedEmitter<E> {
private listeners: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(e: K, h: (p: E[K]) => void) { (this.listeners[e] ||= []).push(h); }
emit<K extends keyof E>(e: K, p: E[K]) { (this.listeners[e] || []).forEach(h => h(p)); }
}
const bus = new TypedEmitter<{ hi: { name: string } }>();
bus.on("hi", p => console.log(p.name));
bus.emit("hi", { name: "Ada" });Frequently asked questions
Is the “Generic Event Emitter Class” lesson free?
Yes — the full text of “Generic Event Emitter Class” is free to read here on the web, and the TypeScript Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the TypeScript Academy course, upgrade to CoddyKit PRO.
What will I learn in “Generic Event Emitter Class”?
Build a reusable typed event emitter from scratch. You practise TypeScript Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start TypeScript Academy?
No prior experience is required. TypeScript Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Generic Event Emitter Class” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this TypeScript Academy lesson?
Yes. Every TypeScript Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Typing Event Maps
- Type-Safe emit and on
- Generic Event Emitter Class
- Inferring Listener Arguments