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.

Or clone locally

git clone -b challenge/2026-07-12 https://github.com/niltonheck/typedrop.git