TypeDrop

2026-08-06 Challenge

2026-08-06 Medium

Typed Paginated API Client with Cursor-Based Iteration

You're building the data-fetching layer for an admin dashboard that consumes a paginated REST API. Responses arrive in cursor-based pages, and the client must lazily iterate through all pages, accumulate typed results, and surface per-page errors as a typed `Result<T, FetchError>` — never throwing, and never losing the shape of the resource being fetched.

Goals

  • Define a discriminated Result<T,E> union and a FetchError variant union with three error kinds.
  • Implement the fetchPages<T> async generator that walks a cursor chain, respects maxPages and AbortSignal, and yields a typed PageOutcome<T> for every fetch attempt.
  • Implement collectAll<T> to drain the generator and accumulate items and errors into a typed summary without ever throwing.
  • Define the ExtractOk<R> conditional type that infers and returns the Ok value type from any Result<T,E>.
challenge.ts
// Core types you must define and implement:

interface Page<T> {
  items: T[];
  nextCursor: string | null;
  total: number;
}

type Result<T, E> =
  | { tag: "ok"; value: T }
  | { tag: "err"; error: E };

type FetchError =
  | { kind: "network"; message: string }
  | { kind: "http"; status: number; body: string }
  | { kind: "parse"; raw: string };

type PageFetcher<T> = (cursor: string | null) => Promise<Result<Page<T>, FetchError>>;

interface PageOutcome<T> {
  result: Result<Page<T>, FetchError>;
  pageIndex: number;
  cursorUsed: string | null;
}

// Main function you must implement:
async function* fetchPages<T>(
  config: PaginatedClientConfig<T>
): AsyncGenerator<PageOutcome<T>, void, unknown> { /* ... */ }
Hints (click to reveal)

Hints

  • For ExtractOk, use `R extends Result<infer T, infer _E> ? T : never` — the `infer` keyword lets you pull T out of the Result structure.
  • An async generator function uses `yield` to emit values and `return` to finish; give it the explicit return type `AsyncGenerator<PageOutcome<T>, void, unknown>` so TypeScript validates every yield site.
  • To narrow a Result inside collectAll, check `outcome.result.tag === 'ok'` — TypeScript will then know `.value` exists and has type `Page<T>`.

Or clone locally

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