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.
Useful resources
Or clone locally
git clone -b challenge/2026-10-20 https://github.com/niltonheck/typedrop.git