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.

Or clone locally

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