TypeDrop
2026-04-30 Challenge
2026-04-30
Medium
Typed Paginated API Client
You're building the data-fetching layer for an analytics dashboard that consumes a paginated REST API. Raw page responses arrive as `unknown` from the network; your client must validate each page, lazily accumulate results with a configurable concurrency limit, and surface a discriminated-union outcome per fetch — with zero `any`.
Goals
- Define `PageResponse<T>` and the three-variant `FetchResult<T>` discriminated union with correct generic typing.
- Implement `validatePageResponse` to structurally narrow an `unknown` payload and apply a per-item type guard, returning descriptive error messages.
- Implement `fetchPage` to wrap an async fetcher, delegate to `validatePageResponse`, and catch rejections into a `'failed'` result.
- Implement `fetchAllPages` to discover pagination from page 1, then fetch remaining pages in concurrency-bounded batches using `Promise.all`, and assemble a typed `FetchAllSummary<T>`.
challenge.ts
// Key types and main function signature
interface PageResponse<T> {
items: T[];
page: number;
pageSize: number;
total: number;
}
type FetchResult<T> =
| { status: "ok"; page: number; items: T[] }
| { status: "invalid"; page: number; error: string }
| { status: "failed"; page: number; error: string };
interface FetchAllOptions { concurrency: number; }
interface FetchAllSummary<T> {
results: FetchResult<T>[];
items: T[];
successCount: number;
failureCount: number;
}
// Core entry point — fetches all pages with bounded concurrency
declare function fetchAllPages<T>(
fetcher: (page: number) => Promise<unknown>,
validator: (item: unknown) => item is T,
options: FetchAllOptions
): Promise<FetchAllSummary<T>>;
Hints (click to reveal)
Hints
- To narrow `unknown` to an object shape without `any`, use `typeof x === 'object' && x !== null` and then index via `(x as Record<string, unknown>).field` — this is the one structural cast the TS compiler accepts under strict mode.
- For concurrency batching, slice the array of remaining page numbers into chunks of size `options.concurrency` and `await Promise.all(chunk.map(...))` sequentially in a loop.
- When accumulating items in page order, sort (or build) your `FetchAllSummary.items` array by the `page` field on each 'ok' result rather than insertion order, since parallel batches may resolve out of order.
Useful resources
Or clone locally
git clone -b challenge/2026-04-30 https://github.com/niltonheck/typedrop.git