TypeDrop

2026-07-22 Challenge

2026-07-22 Medium

Typed Paginated API Client with Result Aggregation

You're building the data-fetching layer for an analytics dashboard. The backend exposes a cursor-based paginated endpoint, and your client must walk every page, accumulate records, handle per-page errors gracefully, and surface a fully-typed aggregated result — all without losing type information or resorting to `any`.

Goals

  • Implement the `ok` and `err` constructors that produce correctly-typed discriminated union values.
  • Implement `fetchAllPages` to walk a cursor chain sequentially, collecting items and errors into an `AggregatedResult<T, E>`.
  • Implement `pluckField` so its return type is inferred as `Array<T[K]>` using a `keyof` constraint and indexed access type.
  • Implement `mapFetchPage` to wrap a `FetchPage<A, E>` and produce a `FetchPage<B, E>` by transforming each page's items, passing errors through unchanged.
challenge.ts
// Core types you'll be working with:

interface Page<T> {
  items: T[];
  nextCursor: string | null; // null = last page
  totalCount: number;
}

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

type FetchPage<T, E> = (cursor: string | undefined) => Promise<Result<Page<T>, E>>;

interface AggregatedResult<T, E> {
  items: T[];
  totalCount: number;
  pagesFetched: number;
  errors: E[];
}

// Main function to implement:
async function fetchAllPages<T, E>(
  fetchPage: FetchPage<T, E>
): Promise<AggregatedResult<T, E>> { /* TODO */ }

// Bonus utilities:
function pluckField<T, E, K extends keyof T>(result: AggregatedResult<T, E>, key: K): Array<T[K]> { /* TODO */ }
function mapFetchPage<A, B, E>(fetchPage: FetchPage<A, E>, transform: (item: A) => B): FetchPage<B, E> { /* TODO */ }
Hints (click to reveal)

Hints

  • For `fetchAllPages`, a `while (cursor !== null)` loop with `cursor` typed as `string | undefined | null` lets you distinguish 'first call' from 'done' naturally.
  • In `pluckField`, the constraint `K extends keyof T` is all you need — TypeScript will infer `T[K]` as the element type of the returned array without any casting.
  • `mapFetchPage` should return an async arrow function that calls the inner `fetchPage`, then checks `result.ok` to decide whether to map items or forward the `Err` unchanged — discriminated union narrowing does the heavy lifting.

Or clone locally

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