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