Typorithm · hard

16. Capstone: a correlated event registry

typescriptstable-typescriptinterviewindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

16. Capstone: a correlated event registry

0%

Concept and contract

Build a small event dispatcher with validated entry points and matching payloads.

A typed event registry connects each event name to its payload. The event data should preserve that relationship even when stored in a queue. An object with independent name and payload unions would admit mismatched combinations, so begin with explicit complete event variants.

The capstone uses two events: roleSaved with a role id, and companyFollowed with a company id. These are learning fixtures, not a production notification service. The goal is a correct relationship and a testable dispatcher, without introducing network delivery or persistence complexity.

Mechanism and worked example

The worked Event union keeps the name and payload together.

The worked Event union keeps the name and payload together. Dispatch narrows on kind before calling the corresponding handler. Each handler receives exactly the payload it expects, and the implementation needs no any cast or broad generic assertion.

A mapped union can derive these variants from a payload map once the explicit version is clear. Indexing the mapping by all keys creates the union of complete records. This is a useful extension, but the explicit two-event form remains a valid readable solution.

Complete worked example

typescript

type Event = { kind: "roleSaved"; payload: { roleId: string } } | { kind: "companyFollowed"; payload: { companyId: string } };
type Handlers = { roleSaved: (payload: {roleId:string}) => void; companyFollowed: (payload: {companyId:string}) => void };
function dispatch(event: Event, handlers: Handlers): void {
  if(event.kind === "roleSaved") handlers.roleSaved(event.payload);
  else handlers.companyFollowed(event.payload);
}
Reasoning
The tagged record and matching handler branch preserve the payload relationship without assertions.

Counterexamples and common mistakes

The rejected roleSaved event carries a company payload.

The rejected roleSaved event carries a company payload. This is the specific mistake the type relationship should prevent. A generic emit<K>(name: K, payload: Map[K]) may become less obvious when K itself is a union, so test stored mixed events rather than only literal calls.

External event input still requires validation before dispatch. A typed registry does not establish that a WebSocket message or stored JSON has a supported tag and payload. Keep parsing separate and never invoke a handler before the relevant fields have been checked.

Rejected counterexample: expected compiler diagnostic

typescript

type Event = { kind: "roleSaved"; payload: { roleId: string } } | { kind: "companyFollowed"; payload: { companyId: string } };
// @ts-expect-error Event name and payload are mismatched.
const invalid: Event = {kind:"roleSaved",payload:{companyId:"c1"}};
Reasoning
A payload map must remain correlated with its event name even when events are stored.

Independent practice

Implement dispatch for the two-event union and a handlers object.

Implement dispatch for the two-event union and a handlers object. Record which handler ran in an array, then validate an unknown roleSaved record before dispatching it. Start with one event and add the second only after the first path works.

Test both events, a malformed payload and an unknown tag. Optional extension: derive the Event union from a Payloads map. The extension should preserve the same negative mismatch test rather than replacing it with a more permissive signature.

Step 1: Validate one external event variant. Step 2: Dispatch each trusted tag to its matching handler.

Practice starter: solve before reading the solution

typescript

type Saved = { kind: "roleSaved"; payload: { roleId: string } };
function parseSaved(raw: unknown): Saved | undefined { throw new Error("TODO"); }
Reasoning
Validate the external payload before allowing it to reach a typed dispatcher.
  • Hint 1: Check payload fields before constructing the event.
  • Hint 2: A stored name and payload must remain correlated.

Solution, tests and trade-offs

The solution uses an explicit switch-like branch and a small parser for the supported external record.

The solution uses an explicit switch-like branch and a small parser for the supported external record. Parsing constructs a fresh event only after checking its id field. A malformed input returns undefined and never reaches a handler.

Runtime assertions verify handler identity and payload delivery. Compiler rejection verifies mismatched static construction. Together they establish the capstone’s limited guarantee; delivery retries, user permissions and durable notification storage would need their own production design.

Practice solution and acceptance checks

typescript

type Saved = { kind: "roleSaved"; payload: { roleId: string } };
function parseSaved(raw: unknown): Saved | undefined {
  if(typeof raw !== "object" || raw === null || !("kind" in raw) || raw.kind !== "roleSaved" || !("payload" in raw)) return undefined;
  const payload = raw.payload;
  if(typeof payload !== "object" || payload === null || !("roleId" in payload) || typeof payload.roleId !== "string") return undefined;
  return {kind:"roleSaved",payload:{roleId:payload.roleId}};
}

// Acceptance checks
if(parseSaved({kind:"roleSaved",payload:{roleId:"r1"}})?.payload.roleId!=="r1"||parseSaved({kind:"other",payload:{roleId:"r1"}})!==undefined||parseSaved({kind:"roleSaved",payload:{companyId:"c1"}})!==undefined)throw new Error("event boundary");
Reasoning
The parser constructs only a checked variant; dispatch and delivery remain separate responsibilities.

Interview reasoning and next step

Present the registry as a sequence of evidence: external input, validated variant, narrowed dispatch and observed handler result.

Present the registry as a sequence of evidence: external input, validated variant, narrowed dispatch and observed handler result. Explain why the simplest explicit union is an acceptable final implementation and when a mapped derivation would reduce real repetition.

Continue to the production course for API decoding, asynchronous state and React. Keep these type-design tools available, but choose them only when they clarify a real application contract rather than making a small feature harder to read.

Optional stretch — after the core task works: Derive the explicit union from a finite payload map.

Build a small event dispatcher with validated entry points and matching payloads.