TypeDrop

2026-02-23 Challenge

2026-02-23 Hard

Typed Reactive State Machine

You're building the core of a checkout flow for an e-commerce platform. The checkout process moves through well-defined states (idle → validating → payment → confirmed / failed), and every transition must be explicitly allowed, carry typed payloads, and notify strongly-typed subscribers — all enforced at compile time.

Goals

  • Define a discriminated `CheckoutEvent` union whose variants carry only the payload each transition needs, and a `TransitionMap` mapped type where `reduce` receives narrowed state and event types — not the full unions.
  • Implement `CheckoutMachine` so that `send()` enforces allowed transitions at runtime, throws a `TypeError` on illegal ones, and notifies subscribers with `(nextState, prevState)`.
  • Implement `subscribe()` returning a working unsubscribe function, and `reset()` as a convenience wrapper that guards against calls from non-terminal states.
  • Implement `cartTotal` using a single `reduce` call and `assertNeverState` as an exhaustive compile-time + runtime safety net.
challenge.ts
export type CheckoutState =
  | IdleState
  | ValidatingState
  | PaymentState
  | ConfirmedState
  | FailedState;

// Each event variant carries only the payload its transition needs:
export type CheckoutEvent = never; // TODO: your discriminated union here

// TransitionMap maps every event `type` to its allowed source states
// and a pure reduce function — narrowed at the type level:
export type TransitionMap = {
  [E in CheckoutEvent as E["type"]]: {
    from: ReadonlyArray<CheckoutState["kind"]>;
    reduce: (state: /* narrowed */ CheckoutState, event: E) => CheckoutState;
  };
};

export class CheckoutMachine {
  constructor(initialCart: CartItem[]);
  getState(): CheckoutState;
  send(event: CheckoutEvent): CheckoutState;
  subscribe(fn: Subscriber): () => void;
  reset(newCart?: CartItem[]): CheckoutState;
}
Hints (click to reveal)

Hints

  • To get narrowed types in `TransitionMap`, try a helper type like `type EventByType<T extends CheckoutEvent["type"]> = Extract<CheckoutEvent, { type: T }>` and use it in the mapped type's `reduce` signature.
  • Use `Extract<CheckoutState, { kind: K }>` to narrow the `state` parameter in each `reduce` function — this keeps the transition table fully type-safe without any casts.
  • Store subscribers in a `Set<Subscriber>` so that returning `() => set.delete(fn)` from `subscribe()` gives you a clean, allocation-light unsubscribe pattern.

Or clone locally

git clone -b challenge/2026-02-23 https://github.com/niltonheck/typedrop.git