typescript 45 lines · 7 steps

A typed checkout state machine in TypeScript

A finite state machine models checkout flow with a transition table the type system keeps honest.

Explained by highlit
1type CheckoutState = "cart" | "shipping" | "payment" | "review" | "confirmed";
2 
3type CheckoutEvent =
4 | { type: "PROCEED" }
5 | { type: "BACK" }
6 | { type: "CANCEL" };
7 
8const transitions: Record<CheckoutState, Partial<Record<CheckoutEvent["type"], CheckoutState>>> = {
9 cart: { PROCEED: "shipping" },
10 shipping: { PROCEED: "payment", BACK: "cart" },
11 payment: { PROCEED: "review", BACK: "shipping" },
12 review: { PROCEED: "confirmed", BACK: "payment" },
13 confirmed: {},
14};
15 
16export class CheckoutMachine {
17 private state: CheckoutState = "cart";
18 
19 get current(): CheckoutState {
20 return this.state;
21 }
22 
23 can(event: CheckoutEvent["type"]): boolean {
24 return event in transitions[this.state] || event === "CANCEL";
25 }
26 
27 send(event: CheckoutEvent): CheckoutState {
28 if (event.type === "CANCEL") {
29 this.state = "cart";
30 return this.state;
31 }
32 
33 const next = transitions[this.state][event.type];
34 if (!next) {
35 throw new Error(`Invalid transition: ${event.type} from ${this.state}`);
36 }
37 
38 this.state = next;
39 return this.state;
40 }
41 
42 isComplete(): boolean {
43 return this.state === "confirmed";
44 }
45}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Modeling states and events as union types lets the compiler reject impossible combinations before runtime.
  2. 2A transition table separates the rules of a workflow from the code that executes them, making both easier to audit.
  3. 3Guarding transitions and centralizing state changes keeps invalid states unreachable by construction.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A typed checkout state machine in TypeScript — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code