TypeDrop

2026-08-13 Challenge

2026-08-13 Medium

Typed Paginated API Client with Cursor-Based Iteration

You're building the data-access layer for an analytics dashboard that streams records from a paginated REST API. The API uses cursor-based pagination, returns heterogeneous resource types, and can fail mid-stream — your client must expose a strongly-typed async generator that yields individual records, handles errors as typed Results, and supports early cancellation via AbortSignal.

Goals

  • Define a discriminated-union Result<T,E> type with Ok/Err variants and a three-way FetchError union identified by a `kind` literal.
  • Implement Page<T> and PaginatedFetchOptions<T,U> with a default generic parameter and an optional per-item transform.
  • Build the paginatedFetch async generator that handles AbortSignal cancellation, network errors, and cursor advancement — yielding typed Results.
  • Implement collectResults to drain the generator into separate values/errors arrays, plus isOk/isErr type-guard functions that correctly narrow the union.
challenge.ts
interface User { id: number; name: string }

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

type FetchError =
  | { kind: "network"; message: string; retryable: boolean }
  | { kind: "parse";   message: string; raw: string }
  | { kind: "abort";   message: string };

type Page<T> = {
  items:       T[];
  nextCursor:  string | null;
  totalCount?: number;
};

type PaginatedFetchOptions<T, U = T> = {
  fetcher:      (cursor: string | null, signal: AbortSignal) => Promise<Page<T>>;
  signal:       AbortSignal;
  transform?:   (item: T) => U;
  startCursor?: string | null;
};

export async function* paginatedFetch<T, U = T>(
  options: PaginatedFetchOptions<T, U>
): AsyncGenerator<Result<U, FetchError>> { /* TODO */ }
Hints (click to reveal)

Hints

  • For the default generic parameter trick in PaginatedFetchOptions, try `transform?: (item: T) => U` — when omitted, the caller gets U = T automatically.
  • An async generator is declared with `async function*`; use `yield` inside a `for...of` loop over `page.items`, and check `signal.aborted` before every fetcher call.
  • To distinguish an AbortError from a generic network error, check `err instanceof DOMException && err.name === 'AbortError'` inside your catch block.

Or clone locally

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