TypeDrop

2026-10-24 Challenge

2026-10-24 Hard

Typed Event-Sourced State Machine

You're building the order-lifecycle engine for a fulfillment platform. Orders move through a strict set of states driven by domain events — the compiler must guarantee that every transition is valid for the current state, every event carries exactly the right payload, and replaying an event log always reconstructs a consistent, fully-typed snapshot.

Goals

  • Define `OrderState` and `OrderEvent` as exhaustive discriminated unions, with each variant carrying only the data meaningful at that stage of the lifecycle.
  • Build `TRANSITIONS` as a narrowly-typed compile-time table using `satisfies`, and derive the `LegalMove` union from it with a conditional type and `infer`.
  • Implement `transition` exhaustively over all state/event combinations — using structural narrowing only (no `as` or `any`) — and throw a typed `MachineError` for every invalid edge.
  • Implement `replay` to reconstruct state by folding an event log, and `project` to extract a `Pick<OrderSnapshot, K>` read-model from any state using a generic key constraint.
challenge.ts

// Key types you'll define and combine:

export type OrderState =
  | { kind: "Pending";   orderId: string; customerId: string; createdAt: Date }
  | { kind: "Confirmed"; orderId: string; customerId: string; createdAt: Date;
      confirmedAt: Date; totalCents: number }
  | { kind: "Shipped";   /* ... cumulative fields ... */ trackingCode: string; shippedAt: Date }
  // | ... more variants

export type OrderEvent =
  | { kind: "PlaceOrder";     orderId: string; customerId: string; createdAt: Date }
  | { kind: "ConfirmPayment"; confirmedAt: Date; totalCents: number }
  // | ... more events

export type Transition<From extends string, Evt extends string, To extends string> = {
  readonly from: From;
  readonly on: Evt;
  readonly to: To;
};

// Compile-time transition table — narrowly typed via `satisfies`:
export const TRANSITIONS = [
  { from: "Pending", on: "ConfirmPayment", to: "Confirmed" },
  // ...
] satisfies ReadonlyArray<Transition<string, string, string>>;

// Core functions to implement:
export function transition(state: OrderState, event: OrderEvent): OrderState { /* TODO */ }
export function replay(events: ReadonlyArray<OrderEvent>): OrderState { /* TODO */ }
export function project<K extends keyof OrderSnapshot>(
  state: OrderState, keys: ReadonlyArray<K>
): Pick<OrderSnapshot, K> { /* TODO */ }
Hints (click to reveal)

Hints

  • Each `OrderState` variant should be *cumulative* — a `Shipped` order still carries `confirmedAt` and `warehouseId` — so `transition` can safely spread earlier state into the new variant.
  • For exhaustiveness in `transition`, nest your `switch` statements: outer on `state.kind`, inner on `event.kind`; TypeScript will flag any unhandled combination as a type error.
  • In `project`, build a full `OrderSnapshot` object from the state first, then use `keys.reduce` (or a for-of loop) to assemble the `Pick` — the generic `K` constraint ensures the return type is exact.

Or clone locally

git clone -b challenge/2026-10-24 https://github.com/niltonheck/typedrop.git