Challenge 3: Build a Tiny Type-Safe State Machine — Possible Solution ==================================================================== // The transition table: for each state, which events are valid and what // state each one leads to. interface Transitions { idle: { start: "running" }; running: { pause: "paused"; stop: "idle" }; paused: { resume: "running"; stop: "idle" }; } type State = keyof Transitions; type EventFor = keyof Transitions[S]; // The runtime companion — must stay structurally in sync with the // Transitions type above (the same "type declaration vs implementation" // discipline from the Module Augmentation chapter's polyfill example). const transitionTable: Transitions = { idle: { start: "running" }, running: { pause: "paused", stop: "idle" }, paused: { resume: "running", stop: "idle" }, }; function transition>( state: S, event: E ): Transitions[S][E] { const nextState = (transitionTable[state] as Record)[event as string]; return nextState as Transitions[S][E]; } // --- Usage --- const s1 = transition("idle", "start"); // "running" — typed as the literal "running" const s2 = transition("running", "pause"); // "paused" const s3 = transition("paused", "resume"); // "running" // transition("idle", "pause"); // ❌ Argument of type '"pause"' is not assignable to parameter of type // '"start"' — "pause" isn't a valid event from the "idle" state. // transition("running", "resume"); // ❌ "resume" isn't valid from "running" either — only "paused" allows it. // A tiny state-driven object built on top: class TrafficLight { private state: State = "idle"; send>(event: E): void { this.state = transition(this.state as State, event as EventFor) as State; console.log(`Now in state: ${this.state}`); } } WHY THIS WORKS -------------- - Transitions is a plain object type where each state's value is itself an object mapping valid event names to the resulting state — this "map of maps" shape means `Transitions[S]` is the set of events valid from state S, and `Transitions[S][E]` is the resulting state for that specific (state, event) pair, computed entirely through indexed access types. - `EventFor = keyof Transitions[S]` constrains the second type parameter of `transition()` to ONLY the event keys valid for whichever state was passed as the first argument — this is what makes `transition("idle", "pause")` a compile error: "pause" simply isn't a key of `Transitions["idle"]`. - The return type `Transitions[S][E]` means the caller doesn't just get back a generic `State` — they get back the SPECIFIC literal state that this exact (state, event) pair transitions to, which is why `s1` above is typed as the literal `"running"`, not the broader union of all states. - The runtime `transitionTable` object is the actual source of truth for behavior; the types describe and constrain it but don't replace it — keeping both in sync (adding a new state/event to both places together) is the same discipline every branded type and polyfill in this course has relied on.