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