TypeDrop

A new TypeScript challenge every day. Sharpen your types.

TypeDrop delivers a fresh TypeScript challenge every day, generated by AI. Pick a challenge, open it in StackBlitz (preferred) or CodeSandbox (or clone it locally), and make the tests pass. No accounts, no setup — just you and the type system.

Learn more on GitHub →

2026-10-17 Medium

Typed Retry Scheduler with Exponential Backoff

You're building the resilience layer for a microservice that calls unreliable third-party APIs. Each operation is declared with a typed retry policy, and the scheduler must execute it with exponential backoff, surface structured typed outcomes for every attempt, and ultimately resolve to a discriminated Result — the compiler must enforce that policy fields, attempt records, and final outcomes are fully consistent with the declared schema.

Goals

  • Define the `Milliseconds` branded type and all discriminated union types (`AttemptRecord<T>`, `SchedulerResult<T>`) with correct discriminant fields.
  • Implement `calcDelay` to compute exponential backoff and clamp the result to `policy.maxDelay`, returning a `Milliseconds` value.
  • Implement `withRetry<T>` to run the operation up to `maxAttempts` times, record every attempt, honour `shouldRetry`, inject the optional `sleep` dependency, and resolve to a correctly typed `SchedulerResult<T>`.
  • Implement the `ExtractSuccess<R>` conditional type that infers and returns `T` when `R` is a succeeded `SchedulerResult<T>`, and `never` otherwise.
challenge.ts
// Key types & main function signature

export type Milliseconds = number & { readonly __brand: "ms" };

export type RetryPolicy = {
  maxAttempts:   number;
  baseDelay:     Milliseconds;
  maxDelay:      Milliseconds;
  backoffFactor: number;
  shouldRetry:   (error: unknown) => boolean;
};

export type AttemptRecord<T> =
  | { status: "success"; attemptNumber: number; value: T;       durationMs: Milliseconds }
  | { status: "failure"; attemptNumber: number; error: unknown; durationMs: Milliseconds };

export type SchedulerResult<T> =
  | { outcome: "succeeded"; value: T;            attempts: AttemptRecord<T>[] }
  | { outcome: "failed";    lastError: unknown;  attempts: AttemptRecord<T>[] };

export async function withRetry<T>(
  operation: () => Promise<T>,
  policy: RetryPolicy,
  sleep?: (delayMs: Milliseconds) => Promise<void>
): Promise<SchedulerResult<T>>;
Hints (click to reveal)

Hints

  • For `ExtractSuccess`, nest two conditional type checks: first check that `R extends SchedulerResult<infer T>`, then check that the `outcome` discriminant is `"succeeded"` before resolving to `T`.
  • To keep `Milliseconds` brand-safe without `as`, the `ms()` helper is the single coercion point — use a type assertion only inside that one helper.
  • For the `sleep` default, construct a real `setTimeout`-based fallback inline inside `withRetry` using a default parameter or an `??` assignment — that way callers in tests can inject `noSleep` to make tests instant.

Or clone locally

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