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