0Pricing
TypeScript Academy · Lesson

Type-Safe emit and on

Enforce correct payloads when emitting and listening.

Type-Safe emit and on is a free TypeScript Academy lesson on CoddyKit — lesson 2 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.

A Type-Safe emit Signature

Make emit generic over the event name K. The payload parameter is then Events[K], so name and payload must match.

interface Events { login: { userId: string }; logout: void }
function emit<K extends keyof Events>(event: K, payload: Events[K]) {
  console.log("emit", event, JSON.stringify(payload));
}
emit("login", { userId: "u1" });

Wrong Payload Is a Compile Error

If the payload does not match the event name, TypeScript rejects it. The generic links the two parameters.

interface Events { login: { userId: string } }
function emit<K extends keyof Events>(event: K, payload: Events[K]) {}
// emit("login", { id: "u1" }); // Error: id is not userId
emit("login", { userId: "u1" });
console.log("ok");

Unknown Event Names Are Rejected

Because K extends keyof Events, you cannot emit an event that is not in the map.

interface Events { login: { userId: string } }
function emit<K extends keyof Events>(event: K, payload: Events[K]) {}
// emit("signup", { userId: "x" }); // Error: "signup" not a key
emit("login", { userId: "x" });
console.log("only known events");

Typed Handlers With on

The on method registers a handler whose argument is exactly Events[K], so handlers receive correctly typed payloads.

interface Events { login: { userId: string }; logout: void }
function on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void) {
  console.log("subscribed to", event);
}
on("login", (p) => console.log(p.userId));

Handler Argument Is Inferred

You do not annotate the handler parameter; TypeScript infers it from the event name, giving full autocomplete inside the handler body.

interface Events { message: { text: string } }
function on<K extends keyof Events>(event: K, handler: (p: Events[K]) => void) {}
on("message", (p) => {
  // p is { text: string }
  console.log(p.text.toUpperCase());
});

Handling Payloadless Events

For void events, the handler simply takes no useful argument. You can call its handler without data.

interface Events { logout: void }
function on<K extends keyof Events>(event: K, handler: (p: Events[K]) => void) {}
on("logout", () => console.log("logged out"));

Combining emit and on

Together, emit and on form a typed pub/sub. Emitting an event delivers a correctly typed payload to its handlers.

interface Events { ping: { at: number } }
type Handler<K extends keyof Events> = (p: Events[K]) => void;
const handlers: { ping: Handler<"ping">[] } = { ping: [] };
function on<K extends keyof Events>(e: K, h: Handler<K>) { handlers[e].push(h as Handler<"ping">); }
function emit<K extends keyof Events>(e: K, p: Events[K]) { handlers[e].forEach(h => h(p as Events["ping"])); }
on("ping", p => console.log("at", p.at));
emit("ping", { at: 5 });

Constraining the Payload Position

The generic ensures the payload argument is checked against the chosen event, catching mismatches at the call site, not deep in a handler.

interface Events { add: { x: number; y: number } }
function emit<K extends keyof Events>(e: K, p: Events[K]) {
  return e;
}
emit("add", { x: 1, y: 2 });
// emit("add", { x: 1 }); // Error: y missing
console.log("payload checked");

Autocomplete for Event Names

Because the first parameter is keyof Events, editors autocomplete valid event names as you type the call.

interface Events { open: void; close: void }
function emit<K extends keyof Events>(e: K, p: Events[K]) {}
// Typing emit(" suggests "open" and "close"
emit("open", undefined as void);
console.log("autocompleted names");

Why Generics Are Essential Here

Without a generic, you would need overloads for every event. One generic signature scales to any number of events automatically.

interface Events { a: { x: number }; b: { y: string } }
function emit<K extends keyof Events>(e: K, p: Events[K]) { return p; }
console.log(emit("a", { x: 1 }), emit("b", { y: "hi" }));

A Reusable Pattern

This emit<K>(event, payload) and on<K>(event, handler) pattern is the core of every type-safe emitter, which we will package into a class next.

interface Events { tick: { n: number } }
function emit<K extends keyof Events>(e: K, p: Events[K]) { return e; }
function on<K extends keyof Events>(e: K, h: (p: Events[K]) => void) { return e; }
console.log(emit("tick", { n: 1 }), on("tick", p => p.n));

Quick Check: Typed emit and on

Test your understanding of type-safe emit and on.

Recap: Type-Safe emit and on

You built generic emit and on signatures where the payload is Events[K], so event names autocomplete, wrong payloads are compile errors, and handler arguments are inferred.

interface Events { login: { userId: string } }
function emit<K extends keyof Events>(e: K, p: Events[K]) { return e; }
console.log(emit("login", { userId: "u1" }));

Frequently asked questions

Is the “Type-Safe emit and on” lesson free?

Yes — the full text of “Type-Safe emit and on” 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 “Type-Safe emit and on”?

Enforce correct payloads when emitting and listening. 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 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Type-Safe emit and on” 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

  1. Typing Event Maps
  2. Type-Safe emit and on
  3. Generic Event Emitter Class
  4. Inferring Listener Arguments
← Back to TypeScript Academy