TypeDrop
2026-07-12 Challenge
2026-07-12
Hard
Typed Finite State Machine Executor
You're building the order-lifecycle engine for an e-commerce platform. Orders move through a strict set of states (Draft → Submitted → Processing → Shipped → Delivered, with cancellation paths), and every transition must be explicitly declared, guard-checked, and side-effected — all with zero runtime surprises and zero `any`.
Goals
- Define the `Transition<S, E>` generic type so that `guard` and `effect` receive the exact narrowed event subtype via `Extract<OrderEvent, { type: E["type"] }>` — with no `any` or `as`.
- Populate `TRANSITIONS` with all seven valid order-lifecycle edges (including the guarded Processing→Cancelled transition), validated with `satisfies`.
- Implement `applyEvent` to look up a matching transition, run an optional guard, invoke the effect in a type-safe way, and return the correct `TransitionResult` discriminated-union variant.
- Implement `runMachine` to drive a sequence of events through `applyEvent`, accumulate a log, and abort on the first failure.
challenge.ts
// Core types at a glance
type OrderState = "Draft" | "Submitted" | "Processing" | "Shipped" | "Delivered" | "Cancelled";
type OrderEvent =
| { type: "SUBMIT"; paymentRef: string }
| { type: "ACCEPT"; warehouseId: string }
| { type: "SHIP"; trackingCode: string }
| { type: "DELIVER"; signedBy: string }
| { type: "CANCEL"; reason: string };
// Each transition descriptor — E["type"] narrows the event in guard/effect
type Transition<S extends OrderState, E extends OrderEvent> = {
from: S;
on: E["type"];
to: OrderState;
guard?:(ctx: OrderContext, event: Extract<OrderEvent, { type: E["type"] }>) => boolean;
effect:(ctx: OrderContext, event: Extract<OrderEvent, { type: E["type"] }>) => void;
};
// Main executor — no `any`, no `as`
function applyEvent(
current: OrderState,
event: OrderEvent,
ctx: OrderContext,
transitions: ReadonlyArray<Transition<OrderState, OrderEvent>>
): TransitionResult { /* TODO */ }
Hints (click to reveal)
Hints
- The trickiest part is calling `t.effect(ctx, event)` without `as`. One clean approach: write a small generic helper `function callTransition<E extends OrderEvent>(t: Transition<OrderState, E>, ctx: OrderContext, event: E)` and invoke it inside a `switch (event.type)` block where each branch reconstructs the matching transition.
- Use `satisfies ReadonlyArray<Transition<OrderState, OrderEvent>>` (not a type annotation) on `TRANSITIONS` — this validates every descriptor while preserving the tuple's literal types for later lookup.
- `Extract<OrderEvent, { type: "SUBMIT" }>` resolves to `{ type: "SUBMIT"; paymentRef: string }` — lean on this in both the `Transition` definition and inside `applyEvent` to avoid widening the event to the full union.
Useful resources
Or clone locally
git clone -b challenge/2026-07-12 https://github.com/niltonheck/typedrop.git