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.

Or clone locally

git clone -b challenge/2026-07-16 https://github.com/niltonheck/typedrop.git