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.
Useful resources
Or clone locally
git clone -b challenge/2026-02-23 https://github.com/niltonheck/typedrop.git