TypeDrop

2026-03-05 Challenge

2026-03-05 Medium

Typed Paginated API Client with Result Chaining

You're building the data-fetching layer for an admin dashboard that queries a paginated REST API. Each endpoint returns a different resource shape, and the client must transparently walk pages, accumulate results, and surface typed errors — all without a single `any`.

Goals

  • Define and use a discriminated-union Result<T,E> type with Ok and Err variants throughout all functions.
  • Implement fetchAllPages to walk paginated cursors sequentially, accumulating items and short-circuiting on the first error.
  • Write runtime validators (validateUser, validateAuditLog, validatePage) that narrow unknown to typed structs using only typeof/in guards — no type assertions.
  • Implement mapResult, flatMapResult, and pipeResults to enable safe, composable result chaining.
challenge.ts
// Key types
export type Ok<T>     = { readonly kind: "ok";  readonly value: T };
export type Err<E>    = { readonly kind: "err"; readonly error: E };
export type Result<T, E> = Ok<T> | Err<E>;

export type ApiError =
  | { readonly kind: "network";   readonly message: string }
  | { readonly kind: "not_found"; readonly resource: string }
  | { readonly kind: "parse";     readonly raw: string };

export interface Page<T> {
  readonly items:      T[];
  readonly nextCursor: string | null;
  readonly total:      number;
}

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

// Core functions you must implement:
export async function fetchAllPages<T>(
  fetcher: PageFetcher<T>
): Promise<Result<T[], ApiError>> { /* TODO */ }

export function mapResult<T, U, E>(
  result: Result<T, E>, fn: (value: T) => U
): Result<U, E> { /* TODO */ }

export function groupById<T extends { id: string }>(
  items: T[]
): ReadonlyMap<string, T> { /* TODO */ }
Hints (click to reveal)

Hints

  • For validateUser, check typeof on each field individually before constructing the User — the compiler will track the narrowed type for you.
  • fetchAllPages should use a while loop driven by nextCursor: start with null, replace it with the cursor from each successful page until it's null again.
  • pipeResults can be solved in a single pass with Array.reduce — accumulate values into an Ok([]) and short-circuit to Err on the first Err you encounter.

Or clone locally

git clone -b challenge/2026-03-05 https://github.com/niltonheck/typedrop.git