TypeDrop

2026-08-21 Challenge

2026-08-21 Medium

Typed Paginated API Client with Cursor-Based Iteration & Result Monad

You're building the data-fetching layer for a feed-based social platform. Timelines, notifications, and search results all arrive in cursor-paginated API responses — your client must iterate pages lazily, surface typed success/failure results per page, and aggregate items across pages into a single strongly-typed collection without ever widening to `unknown` unsafely.

Goals

  • Implement the Result<T,E> monad constructors (ok, err) and the isOk type guard so it correctly narrows the discriminated union.
  • Define the Cursor branded type and makeCursor factory so plain strings are never assignable to Cursor without an explicit call.
  • Implement fetchAllPages<T> to iterate pages sequentially, stop on the first error, and return a FetchAllResult<T> with partial items, page count, totalCount, and the error.
  • Implement mapFetcher and withRetry as generic higher-order functions that transform or wrap a PageFetcher<T> without losing type information or swallowing errors.
challenge.ts
// Key types & main function signature

type Ok<T>  = { readonly status: "ok";  readonly value: T };
type Err<E> = { readonly status: "err"; readonly error: E };
type Result<T, E> = Ok<T> | Err<E>;

type Cursor = string & { readonly __cursorBrand: unique symbol };

type Page<T> = {
  readonly items: ReadonlyArray<T>;
  readonly nextCursor: string | null;
  readonly totalCount?: number;
};

type FetchError = {
  readonly kind: "network" | "timeout" | "parse" | "auth";
  readonly message: string;
  readonly retryable: boolean;
};

type PageFetcher<T> = (
  cursor: Cursor | undefined
) => Promise<Result<Page<T>, FetchError>>;

// Sequentially drain all pages and aggregate results
async function fetchAllPages<T>(
  fetcher: PageFetcher<T>
): Promise<FetchAllResult<T>> { /* TODO */ }
Hints (click to reveal)

Hints

  • A branded type is just an intersection with a unique phantom property — `unique symbol` as a property type is the idiomatic TS approach.
  • For withRetry, a simple recursive or loop-based approach works — check `FetchError.retryable` before deciding to recurse.
  • mapFetcher only needs to touch the `items` array inside an Ok<Page<A>> — forward Err results untouched by checking isOk first.

Or clone locally

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