TypeDrop

2026-06-03 Challenge

2026-06-03 Hard

Typed Workflow State Machine Executor

You're building the execution engine for a no-code automation platform where users define multi-step workflows as JSON. Raw workflow definitions arrive as `unknown` from a REST API; your engine must validate them, execute each step through a typed strategy registry, enforce per-step retry logic with exponential back-off, and produce a strongly-typed execution report — with zero `any`.

Goals

  • Define `StepDefinition` as a four-variant discriminated union and wire `StepOutput<K>` and `HandlerRegistry` as mapped types that use `Extract` to maintain per-kind type safety.
  • Implement `parseWorkflowDefinition` to validate an `unknown` payload into a fully-typed `WorkflowDefinition`, throwing descriptive errors for any structural or semantic violations.
  • Implement `withRetry` as a generic async helper with exponential back-off that respects an `AbortSignal` and returns both the result and total attempt count.
  • Implement `executeWorkflow` to run steps sequentially with retry logic, handle condition-step branching, propagate abort/timeout to remaining steps, and produce a complete `WorkflowExecutionReport` including per-kind summary tallies.
challenge.ts
// Key types & main function signatures

type StepDefinition =
  | { id: StepId; kind: "http";      retries: number; url: string; method: "GET"|"POST"|"PUT"|"DELETE"; body?: string }
  | { id: StepId; kind: "transform"; retries: number; expression: string }
  | { id: StepId; kind: "condition"; retries: number; expression: string; onTrue: StepId; onFalse: StepId }
  | { id: StepId; kind: "notify";    retries: number; channel: "email"|"slack"|"sms"; message: string };

// Maps each kind → its handler function (typed per-variant)
type HandlerRegistry = { [K in StepDefinition["kind"]]:
  StepHandler<Extract<StepDefinition, { kind: K }>> };

// Maps each kind → its output shape
type StepOutput<K extends StepDefinition["kind"]> = /* mapped type */ ...;

// Validate unknown → WorkflowDefinition or throw
function parseWorkflowDefinition(raw: unknown): WorkflowDefinition;

// Retry with exponential back-off + AbortSignal support
async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries: number,
  baseDelayMs: number,
  signal: AbortSignal
): Promise<{ result: T; attempts: number }>;

// Run the workflow, return a full execution report
async function executeWorkflow(
  rawDefinition: unknown,
  registry: HandlerRegistry
): Promise<WorkflowExecutionReport>;
Hints (click to reveal)

Hints

  • For `StepOutput<K>` and `HandlerRegistry`, a mapped type of the form `{ [K in StepDefinition["kind"]]: ... Extract<StepDefinition, { kind: K }> ... }` lets TypeScript resolve the exact variant per key — no overloads needed.
  • In `withRetry`, wrap each retry delay in a `Promise` that rejects early when `signal.addEventListener('abort', ...)` fires, then `clearTimeout` in the cleanup to avoid leaks.
  • Inside `executeWorkflow`, maintain a `nextStepIndex` cursor; after a successful 'condition' step, scan `steps` for the index whose `id` matches `output.nextStepId` and jump there — steps in between get a 'skipped' result entry.

Or clone locally

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