TypeDrop

2026-07-08 Challenge

2026-07-08 Medium

Typed Paginated API Client with Result Accumulation

You're building the data-fetching layer for an analytics dashboard that pulls records from a cursor-based REST API. Raw pages arrive as `unknown` from each fetch; your client must validate each page, accumulate typed records across pages, respect a configurable item cap, and surface per-page errors without aborting the entire run — all with zero `any`.

Goals

  • Implement `validatePage` to narrow all `unknown` fields of a `RawPage` into a fully-typed `ValidPage`, returning a `Result` without throwing.
  • Implement `fetchAllPages` to drive the cursor loop, collecting records from each valid page and stopping on network errors, exhausted cursors, or page limits.
  • Enforce the `maxItems` cap by slicing the final page's records to fit exactly, emitting a `cap-reached` `PageError`, and halting pagination.
  • Surface per-page validation and network failures as typed `PageError` entries in `AccumulationResult.errors` without aborting the entire run where possible.
challenge.ts
export interface AnalyticsRecord {
  id: string;
  eventName: string;
  userId: string;
  timestampMs: number;
  metadata: Record<string, string>;
}

export type PageError =
  | { kind: "validation"; pageIndex: number; reason: string }
  | { kind: "network";    pageIndex: number; message: string }
  | { kind: "cap-reached"; itemsFetched: number; cap: number };

export type Result<T, E> =
  | { ok: true;  value: T }
  | { ok: false; error: E };

export type PageFetcher = (
  cursor: string | null
) => Promise<Result<RawPage, string>>;

// Validate one raw API page into a typed ValidPage
export function validatePage(raw: RawPage): Result<ValidPage, string> { ... }

// Accumulate records across cursor-paginated pages
export async function fetchAllPages(
  fetcher: PageFetcher,
  config: PaginationConfig
): Promise<AccumulationResult> { ... }
Hints (click to reveal)

Hints

  • When narrowing `unknown` fields, use `typeof`, `Array.isArray`, and `Object.keys` — never cast with `as`.
  • A discriminated union on `kind` lets you handle each `PageError` variant exhaustively; lean on that in your accumulation loop.
  • For the `metadata` field, iterate `Object.entries` and verify every value `typeof v === 'string'` before constructing the `Record<string, string>`.

Or clone locally

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