Syncing account progress…

Back

Advanced · 18 min

Typed event contracts

Derive correlated event messages and handlers from one event map.

Derive a union of complete messages

Mapping each event key to a message and then indexing the result creates a discriminated union. Each event name stays tied to its own payload. An object with independent unions for name and payload would lose that relationship.

type Events = { opened: { page: string }; scored: { points: number } };
type EventMessage = { [K in keyof Events]: { type: K; payload: Events[K] } }[keyof Events];
function describe(event: EventMessage): string {
  return event.type === "opened" ? event.payload.page : String(event.payload.points);
}
const message: EventMessage = { type: "scored", payload: { points: 5 } };
console.log(describe(message));

Output

5

Check a handler table

A mapped handler type requires a callback for each event with the matching payload. The example invokes a known handler directly. A general dynamic dispatcher needs to preserve the same correlation; an assertion should not hide a mismatch.

type Events = { opened: { page: string }; scored: { points: number } };
type Handlers = { [K in keyof Events]: (payload: Events[K]) => string };
const handlers: Handlers = { opened: data => data.page, scored: data => String(data.points) };
console.log(handlers.opened({ page: "Learn" }));

Output

Learn

Put it into practice

  1. Predict the output and explain which relationships the types enforce.
  2. Fix the task while preserving its type contract, then check both practice questions.

Try it yourself

Code runs on this device. When you are signed in, drafts sync to your account.

Fix an event payload

Give the scored message a payload with "points" set to 5. Keep the derived message union.

Show solution
type Events = { opened: { page: string }; scored: { points: number } };
type EventMessage = { [K in keyof Events]: { type: K; payload: Events[K] } }[keyof Events];
function describe(event: EventMessage): string {
  return event.type === "opened" ? event.payload.page : String(event.payload.points);
}
const message: EventMessage = { type: "scored", payload: { points: 5 } };
console.log(describe(message));

The mapped union checks complete event variants. The corrected payload belongs to the scored variant, so the narrowed handler can safely read points.

Practice