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.
Useful resources
Or clone locally
git clone -b challenge/2026-06-03 https://github.com/niltonheck/typedrop.git