TypeDrop

2026-10-09 Challenge

2026-10-09 Medium

Typed Retry-with-Fallback Async Executor

You're building the resilience layer for a microservice that calls unreliable third-party APIs. Each operation declares its own retry policy, a typed fallback strategy, and a structured error taxonomy — the compiler must enforce that fallback return types are compatible with the primary operation, that error kinds are exhaustively handled, and that the final resolved value is correctly narrowed.

Goals

  • Define the `OpError` discriminated union, `Result<T,E>` generic type, `RetryPolicy`, and `FallbackStrategy<T>` discriminated union with correct TypeScript shapes.
  • Implement `runWithRetry<T>` that retries only on listed error kinds, waits between attempts, and dispatches the correct fallback strategy when retries are exhausted.
  • Implement `classifyError` with exhaustive narrowing over all `OpError` variants using a never-guard.
  • Implement `summariseResults` that returns a `Record<OpError['kind'], number>` with all three keys always present, even when zero.
challenge.ts
// Key types and main function signature

type OpError =
  | { kind: "network"; statusCode: number; message: string }
  | { kind: "timeout"; elapsedMs: number; message: string }
  | { kind: "validation"; fields: string[]; message: string };

type Result<T, E> =
  | { ok: true; value: T }
  | { ok: false; error: E };

type FallbackStrategy<T> =
  | { type: "static"; value: T }
  | { type: "compute"; fn: () => Promise<T> }
  | { type: "rethrow" };

interface Operation<T> {
  exec: () => Promise<Result<T, OpError>>;
  policy: RetryPolicy;
  fallback: FallbackStrategy<T>;
}

async function runWithRetry<T>(
  op: Operation<T>
): Promise<Result<T, OpError>> { ... }
Hints (click to reveal)

Hints

  • For exhaustive matching in `classifyError`, assign the default case to a `const _: never` variable — the compiler will error if any variant is unhandled.
  • The `FallbackStrategy<T>` type parameter must flow into `Operation<T>` so that a `static` fallback's `value` type matches `T` — no casting needed if the union is defined generically.
  • `Record<OpError['kind'], number>` forces you to initialise all three keys (`network`, `timeout`, `validation`) before returning from `summariseResults`.

Or clone locally

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