TypeDrop
2026-07-16 Challenge
2026-07-16
Medium
Typed Paginated API Client with Result Accumulation
You're building the data-sync engine for an analytics dashboard that pulls records from a paginated REST API. Each page of results must be fetched sequentially, validated from `unknown` into a typed shape, and accumulated into a single `PagedResult<T>` — stopping early on fatal errors and surfacing per-page failures without losing successfully fetched data.
Goals
- Define `PageToken` as a branded string type and `SyncError` as a discriminated union of `ValidationError` and `FetchError`.
- Implement `validateRecord<T>` as a type-guard that checks every key in a `Schema<T>` against the raw object using `Record<string, unknown>` — no `any`.
- Implement `fetchAllPages<T>` to sequentially page through results, validate each record, and accumulate both valid items and per-page errors without ever throwing.
- Ensure fetcher failures are captured as a `FetchError` (with the correct 1-based page number) and stop further fetching while preserving all previously collected data.
challenge.ts
// Core types and main function signature
type Product = { id: number; name: string; inStock: boolean };
// Branded page-token — can't accidentally pass a plain string
export type PageToken = string & { readonly __brand: "PageToken" };
// Discriminated error union
export type SyncError =
| { kind: "validation"; field: string; expected: string; received: string }
| { kind: "fetch"; page: number; message: string };
// Schema maps each key of T to its expected `typeof` string
export type Schema<T> = { [K in keyof T]: string };
// Accumulated result — never throws, always returns this shape
export type PagedResult<T> = {
items: T[];
errors: SyncError[];
totalPagesFetched: number;
};
// Injected fetcher — returns unvalidated unknown records
export type Fetcher = (token: PageToken | undefined) => Promise<PagedResponse<unknown>>;
// Main entry point — sequential pagination + validation + accumulation
export async function fetchAllPages<T>(
fetcher: Fetcher,
schema: Schema<T>
): Promise<PagedResult<T>> { /* TODO */ throw new Error("Not implemented"); }
Hints (click to reveal)
Hints
- To cast `unknown` to an indexable shape without `any`, use `Record<string, unknown>` — first check `typeof raw === 'object' && raw !== null`.
- The `Schema<T>` type uses a mapped type over `keyof T`; when iterating its keys at runtime, `Object.keys(schema)` gives you strings — cast them to `keyof T` only after you've confirmed the object has that key.
- Track `totalPagesFetched` only when a fetcher call *completes successfully* — an error on page N means N-1 pages were fully fetched.
Useful resources
Or clone locally
git clone -b challenge/2026-07-16 https://github.com/niltonheck/typedrop.git