TypeDrop
2026-07-30 Challenge
2026-07-30
Medium
Typed Pagination Aggregator with Cursor-Based Fetching
You're building the data-loading layer for an analytics dashboard that must pull all records from cursor-paginated REST APIs (think GitHub, Stripe, or Notion). Each endpoint returns a typed page of items plus an opaque next-cursor, and you need a generic aggregator that fetches all pages sequentially, enforces a per-fetch timeout via AbortController, and returns a strongly-typed settled result — collected items or a structured error — without any unsafe escape hatches.
Goals
- Define `Page<T>`, `PaginatorConfig<T>`, `FetchError`, and `AggregatorResult<T>` as fully typed, generic discriminated unions with no `any`.
- Implement `aggregatePages<T>` to fetch pages sequentially, race each fetch against a per-page `AbortController` timeout, and return a typed `AggregatorResult<T>` — never throwing.
- Handle all four `FetchError` kinds exhaustively: `timeout`, `aborted`, `network`, and `max_pages`.
- Implement `paginatorFor<T>` as a typed builder that wraps a URL and `transform` function into a valid `PaginatorConfig<T>` with sensible defaults.
challenge.ts
// Core types and main function signature
type Widget = { id: number; name: string };
type Page<T> = {
items: T[];
nextCursor: string | null;
};
type FetchError =
| { kind: "timeout"; pageIndex: number }
| { kind: "aborted"; pageIndex: number }
| { kind: "network"; pageIndex: number; message: string }
| { kind: "max_pages"; limit: number };
type AggregatorResult<T> =
| { status: "ok"; items: T[]; pagesFetched: number }
| { status: "error"; reason: FetchError; pagesFetched: number; partialItems: T[] };
type PaginatorConfig<T> = {
fetchPage: (cursor: string | null, signal: AbortSignal) => Promise<Page<T>>;
pageTimeoutMs: number;
maxPages: number;
};
async function aggregatePages<T>(
config: PaginatorConfig<T>
): Promise<AggregatorResult<T>> { /* TODO */ }
Hints (click to reveal)
Hints
- To race a fetch against a timeout, create an `AbortController`, schedule `controller.abort()` with `setTimeout`, then `Promise.race` the real fetch (passing `controller.signal`) against a promise that rejects on abort — check `signal.aborted` or catch `DOMException` with name `'AbortError'` to distinguish timeout from external abort.
- Use `instanceof DOMException && err.name === 'AbortError'` inside your catch block to tell timeouts/aborts apart from generic network errors — then check the outer timeout controller's signal to know which it was.
- The `transform` callback in `paginatorFor` is the boundary where `unknown` becomes `Page<T>` — keep it typed as `(raw: unknown) => Page<T>` and let callers own the narrowing, so your builder stays free of `any`.
Useful resources
Or clone locally
git clone -b challenge/2026-07-30 https://github.com/niltonheck/typedrop.git