TypeDrop

2026-10-20 Challenge

2026-10-20 Hard

Typed Paginated API Client with Cursor-Based Pagination

You're building the data-access layer for an analytics dashboard that fetches large datasets from a REST API using cursor-based pagination. The client must be fully generic over the resource shape, enforce that every page request and response is structurally consistent, accumulate results across pages with a typed concurrency limit, and surface every failure through a discriminated Result type — the compiler must make it impossible to misuse the cursor, mix up resource types, or silently swallow errors.

Goals

  • Define all branded types, generic interfaces, and the discriminated FetchError union with zero use of `any`.
  • Implement `fetchAllPages<T>` so it fetches all pages sequentially/concurrently, respects the concurrency limit, races each request against a per-request timeout, and returns a typed Result.
  • Propagate errors correctly: classify thrown values into the right FetchError variant and short-circuit on the first failure.
  • Implement `exhaustiveFetchError` with a `never`-based exhaustiveness check that formats all four FetchError variants into the exact required strings.
challenge.ts
// Core types and main function signature

export type Cursor = string & { readonly __brand: "Cursor" };

export interface Page<T> {
  items: T[];
  nextCursor: Cursor | null;
  totalCount: number;
}

export type PageFetcher<T> = (
  cursor: Cursor | null,
  pageSize: number
) => Promise<Page<T>>;

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

export type FetchError =
  | { kind: "NetworkError"; message: string; statusCode: number }
  | { kind: "ParseError";   message: string; page: number }
  | { kind: "CursorError";  message: string; invalidCursor: string }
  | { kind: "TimeoutError"; message: string; afterMs: number };

export async function fetchAllPages<T>(
  options: PaginatedClientOptions<T>
): Promise<Result<AccumulatedResult<T>, FetchError>>;
Hints (click to reveal)

Hints

  • For the timeout race, create a helper like `withTimeout<T>(p: Promise<T>, ms: number, afterMs: number): Promise<T>` that uses `Promise.race` against a `new Promise((_, reject) => setTimeout(...))` — type the rejection as `TimeoutError` before throwing.
  • To classify a caught `unknown` value as a `FetchError`, write a type-guard `isFetchError(e: unknown): e is FetchError` that checks `typeof e === 'object' && e !== null && 'kind' in e` and that the `kind` is one of the four known strings.
  • For the chunked concurrency limit, collect the cursors for all remaining pages upfront, then slice them into batches of size `concurrencyLimit` and `await Promise.all(batch)` one batch at a time — but remember you may not know all cursors in advance (cursor-based!), so you'll need to chain pages sequentially or use a sliding window.

Or clone locally

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