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

typescript
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';

Wiring validation and Swagger docs in NestJS

validation openapi decorators
Intermediate 8 steps
typescript
import { Component, Input } from '@angular/core';
 
interface Order {
  id: string;

How Angular ICU plurals localize an order summary

i18n pluralization standalone-component
Intermediate 8 steps
typescript
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable, catchError, concatMap, finalize } from 'rxjs';
import { DataSource, QueryRunner } from 'typeorm';
 

Wrapping requests in a transaction with NestJS

interceptors transactions rxjs
Advanced 7 steps
javascript
import { useReducer, useEffect } from "react";
 
const initialState = { status: "idle", data: null, error: null };
 

Building a data-fetching hook in React

custom-hooks usereducer data-fetching
Intermediate 9 steps
typescript
type CsvColumn<T> = {
  header: string;
  value: (row: T) => string | number | boolean | null | undefined;
};

Building a type-safe CSV writer in TypeScript

generics serialization escaping
Intermediate 7 steps
typescript
import { parsePhoneNumberFromString, CountryCode } from 'libphonenumber-js';
 
export interface NormalizedPhone {
  e164: string;

Normalizing phone numbers to E.164 in TypeScript

validation normalization error-handling
Intermediate 7 steps

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