TypeDrop
2026-06-06 Challenge
2026-06-06
Hard
Typed Paginated API Client with Retry & Result Monad
You're building the data-access layer for an analytics dashboard that pulls records from a paginated third-party REST API. The API is unreliable — responses arrive as `unknown`, pages must be fetched concurrently up to a configurable limit, transient failures must be retried with exponential back-off, and every outcome must be surfaced through a typed `Result<T, E>` monad — with zero `any`.
Goals
- Implement a fully-typed Result<T,E> monad (Ok/Err constructors + mapResult) and a three-variant ApiError discriminated union.
- Write a generic validatePageResponse<T> that narrows unknown → PageResponse<T> using a caller-supplied type-guard predicate.
- Implement withRetry<T> that retries a Result-returning async operation with exponential back-off and surfaces ExhaustedError after all attempts.
- Implement fetchAllPages<T> that fetches pages 1..N concurrently (capped at a configurable limit), retries failures, and merges results into a FetchReport<T> preserving page order.
challenge.ts
// Core types & main function signature at a glance
type Ok<T> = { readonly tag: "ok"; readonly value: T };
type Err<E> = { readonly tag: "err"; readonly error: E };
type Result<T, E> = Ok<T> | Err<E>;
type ApiError =
| { kind: "NetworkError"; message: string }
| { kind: "ParseError"; message: string; raw: unknown }
| { kind: "ExhaustedError"; attempts: number; lastMessage: string };
type PageResponse<T> = {
page: number;
totalPages: number;
items: readonly T[];
};
type PaginatorOptions<T> = {
totalPages: number;
concurrency: number;
retry: RetryOptions;
fetchPage: (page: number) => Promise<unknown>;
itemGuard: (u: unknown) => u is T;
};
// Main entry point — fetch all pages with concurrency + retry
async function fetchAllPages<T>(
opts: PaginatorOptions<T>
): Promise<FetchReport<T>> { /* TODO */ }
Hints (click to reveal)
Hints
- Model Result<T,E> as a discriminated union on a `tag` field — narrowing with `if (r.tag === 'ok')` then gives you the typed value with no assertions needed.
- For the concurrency cap in fetchAllPages, maintain a Set (or array) of in-flight Promises and await the first to settle before launching the next when the pool is full — Promise.race is your friend here.
- To enforce exhaustiveness in renderApiError, add a `default` branch that assigns `error` to a `never`-typed variable; the compiler will error if any ApiError variant is unhandled.
Useful resources
Or clone locally
git clone -b challenge/2026-06-06 https://github.com/niltonheck/typedrop.git