TypeDrop

2026-03-09 Challenge

2026-03-09 Hard

Typed State Machine Executor with Transition Guards

You're building the order-lifecycle engine for a fulfilment platform. Each order moves through a strict set of states (e.g. `pending → confirmed → shipped → delivered`), and every transition must pass a typed guard before it fires. The engine must enforce exhaustive state/event coverage at the type level, accumulate a typed audit log, and surface a discriminated `Result` for every attempted transition — with zero `any`.

Goals

  • Define a branded `OrderId` type and a safe factory function using a `unique symbol` brand.
  • Implement the `StateMachine` class so it correctly resolves transitions, runs typed guards in order, and appends discriminated `AuditEntry` records to an internal log.
  • Implement `replayToState` so it drives the machine through a sequence of events and returns a fully typed `ReplaySummary` with success/failure breakdowns.
  • Implement `buildTransitionMap` as a generic dual-constraint helper that preserves literal target types at the call site without any type assertions.
challenge.ts
// Core types at a glance

type OrderState = "pending" | "confirmed" | "processing" | "shipped" | "delivered" | "cancelled";
type OrderEvent = "CONFIRM" | "START_PROCESSING" | "SHIP" | "DELIVER" | "CANCEL";

type Guard = (ctx: OrderContext) => true | string; // true = allow, string = deny reason

interface TransitionDef<S extends OrderState> {
  readonly target: Exclude<OrderState, S>; // can't transition to yourself
  readonly guards?: readonly Guard[];
}

type TransitionMap = {
  readonly [S in OrderState]?: {
    readonly [E in OrderEvent]?: TransitionDef<S>;
  };
};

// Main class signature
class StateMachine {
  constructor(map: TransitionMap);
  transition(ctx: OrderContext, currentState: OrderState, event: OrderEvent): TransitionResult;
  getAuditLog(): readonly AuditEntry[];
  getAuditLogFor(orderId: OrderId): readonly AuditEntry[];
}
Hints (click to reveal)

Hints

  • The `Exclude<OrderState, S>` on `TransitionDef.target` is enforced at the type level — lean on it to see how the generic `S` flows through the mapped type.
  • For `buildTransitionMap`, a single `return map` is the entire implementation — the heavy lifting is in the dual generic constraint `T extends TransitionMap` combined with the parameter type `T & TransitionMap`.
  • When narrowing `TransitionResult` in `replayToState`, use the `ok` discriminant to update your running state only on success, keeping the final state stable after a failure.

Or clone locally

git clone -b challenge/2026-03-09 https://github.com/niltonheck/typedrop.git