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