TypeDrop
2026-07-31 Challenge
2026-07-31
Hard
Typed Event-Sourced State Machine
You're building the order-lifecycle engine for an e-commerce platform. Orders move through a strict set of states (e.g. `Pending → Confirmed → Shipped → Delivered`, with cancellation possible from some states), and every transition must be recorded as an immutable event in an append-only log. The hardest part is making TypeScript enforce — at the type level — which events are legal from each state, so invalid transitions are caught at compile time, not at runtime.
Goals
- Define `TransitionMap` as a mapped type over `OrderState` and provide a `TRANSITIONS` constant that `satisfies` it, preserving literal tuple types.
- Implement `LegalEvents<S>` as a conditional/indexed-access type that resolves to the exact `OrderEvent` sub-union legal from state `S`, making illegal `applyEvent` calls compile-time errors.
- Implement `createOrder`, `applyEvent`, `isTerminal`, and `getEventsByType` with correct generic constraints, type predicates, and immutability — without `any` or unsafe casts.
- Implement `rehydrate` to replay an event log, using `TRANSITIONS` for runtime transition validation and throwing descriptive errors for illegal sequences.
challenge.ts
/** All possible order states. */
type OrderState = "Pending" | "Confirmed" | "Shipped" | "Delivered" | "Cancelled" | "Refunded";
type BaseEvent<T extends string, P extends object = Record<string, never>> =
{ type: T; occurredAt: string } & P;
type OrderConfirmed = BaseEvent<"OrderConfirmed", { confirmedBy: string }>;
type OrderShipped = BaseEvent<"OrderShipped", { trackingNumber: string; carrier: string }>;
type OrderCancelled = BaseEvent<"OrderCancelled", { reason: string }>;
// ... (OrderPlaced, OrderDelivered, OrderRefunded follow the same pattern)
type OrderEvent = OrderPlaced | OrderConfirmed | OrderShipped
| OrderDelivered | OrderCancelled | OrderRefunded;
/** Resolves to the union of events legally applicable in state S. */
type LegalEvents<S extends OrderState> = Extract<OrderEvent, { type: typeof TRANSITIONS[S][number] }>;
// LegalEvents<"Pending"> → OrderConfirmed | OrderCancelled
// LegalEvents<"Delivered"> → OrderRefunded
// LegalEvents<"Cancelled"> → never
function applyEvent<S extends OrderState>(
aggregate: OrderAggregate & { state: S },
event: LegalEvents<S> // ← illegal events are compile-time errors
): OrderAggregate { /* ... */ }
Hints (click to reveal)
Hints
- For `LegalEvents<S>`, index into `typeof TRANSITIONS[S][number]` to get the union of allowed type-string literals, then use `Extract<OrderEvent, { type: ... }>` to narrow to the matching event sub-types.
- Use `satisfies TransitionMap` (not `: TransitionMap`) on `TRANSITIONS` so TypeScript widens to `TransitionMap` for assignability checks but keeps the literal tuple types — this is what makes `typeof TRANSITIONS[S][number]` work.
- In `rehydrate`, you can't track the generic `S` across loop iterations; use `TRANSITIONS[aggregate.state].includes(event.type)` as a runtime guard, then use a single type assertion on the `applyEvent` call (with a comment explaining the safety invariant).
Useful resources
Or clone locally
git clone -b challenge/2026-07-31 https://github.com/niltonheck/typedrop.git