TypeDrop
2026-09-11 Challenge
2026-09-11
Medium
Typed Paginated API Client with Retry & Result Aggregation
You're building the data-fetching layer for an analytics dashboard that pulls records from a paginated REST API. Pages must be fetched sequentially, transient errors should be retried with exponential back-off, and the final result must be a typed aggregate — all without a single `any` or unsafe cast.
Goals
- Implement the `Result<T, E>` discriminated union and its `ok` / `err` convenience constructors.
- Implement `withRetry` so it transparently wraps any async `Result`-returning function with exponential back-off and a configurable retry limit.
- Implement `fetchAllPages` to drive a cursor-based pagination loop, wiring in `withRetry` on every page fetch and collecting all items into a flat array.
- Implement `aggregateRecords` using the constrained generic `T extends Record<string, unknown>` and `keyof T` to group items into a `Map` by an arbitrary typed key.
challenge.ts
// Key types + main function signatures
export type Ok<T> = { readonly tag: "ok"; readonly value: T };
export type Err<E> = { readonly tag: "err"; readonly error: E };
export type Result<T, E> = Ok<T> | Err<E>;
export type ApiError = { readonly code: number; readonly message: string };
export type Page<T> = { readonly items: T[]; readonly nextCursor: string | null };
/** Typed async callback: cursor → Result-wrapped page */
export type FetchPage<T> = (cursor: string | null) => Promise<Result<Page<T>, ApiError>>;
/** Retry wrapper: exponential back-off, returns first Ok or final Err */
export async function withRetry<T>(
fn: () => Promise<Result<T, ApiError>>,
maxRetries: number
): Promise<Result<T, ApiError>> { /* TODO */ }
/** Pagination driver: collects all items across pages via withRetry */
export async function fetchAllPages<T>(
fetchPage: FetchPage<T>
): Promise<Result<T[], ApiError>> { /* TODO */ }
/** Group collected items by the string value of a typed key */
export function aggregateRecords<T extends Record<string, unknown>>(
items: T[],
groupKey: keyof T
): Map<string, T[]> { /* TODO */ }
Hints (click to reveal)
Hints
- The `withRetry` delay can be expressed as `100 * 2 ** attempt` ms — use a small `sleep` helper built with `new Promise(resolve => setTimeout(resolve, ms))`.
- In `fetchAllPages`, keep a `cursor: string | null = null` variable and update it to `page.nextCursor` each iteration; exit the loop when it is `null` after the first page is processed.
- Inside `aggregateRecords`, write `String(item[groupKey])` to safely coerce the indexed value to a string without reaching for `any`.
Useful resources
Or clone locally
git clone -b challenge/2026-09-11 https://github.com/niltonheck/typedrop.git