TypeDrop
2026-03-11 Challenge
2026-03-11
Medium
Typed Paginated API Client with Result Chaining
You're building the data-fetching layer for an admin dashboard that pulls paginated records from a REST API. Each page arrives as raw `unknown` JSON, must be validated into a typed shape, and pages must be lazily fetched until exhausted — all surfaced through a `Result<T, E>` type so callers never face surprise runtime exceptions.
Goals
- Define the generic `Result<T, E>` discriminated union and its `ok()` / `err()` constructor helpers.
- Implement `parsePage<T>` to validate raw `unknown` API responses into a typed `Page<T>`, rejecting bad shapes with a branded `ParseError`.
- Implement `fetchAllPages<T>` to lazily follow `nextPage` links, accumulate items, and surface the first failure as a branded `FetchError`.
- Implement the `mapResult` and `chainResult` combinators to enable type-safe transformation and flat-mapping of `Result` values.
challenge.ts
// Key types & main function signature
export type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
export type Page<T> = {
items: T[];
page: number;
nextPage?: number;
};
declare const ParseErrorBrand: unique symbol;
export type ParseError = { message: string; [ParseErrorBrand]: true };
declare const FetchErrorBrand: unique symbol;
export type FetchError = { message: string; cause?: unknown; [FetchErrorBrand]: true };
/** Validates raw unknown data into a typed Page<T>. */
export function parsePage<T>(
raw: unknown,
parseItem: (item: unknown) => Result<T, ParseError>
): Result<Page<T>, ParseError> { /* TODO */ throw 0; }
/** Fetches every page, collects all items, returns Result or first error. */
export async function fetchAllPages<T>(
fetcher: (page: number) => Promise<unknown>,
parseItem: (item: unknown) => Result<T, ParseError>
): Promise<Result<T[], FetchError>> { /* TODO */ throw 0; }
Hints (click to reveal)
Hints
- Branded types use a `unique symbol` as a key — add it to the object type so the brand is structurally unforged.
- In `parsePage`, use `Array.isArray` for the items check and `typeof x === 'number'` for numeric fields — TypeScript will narrow correctly inside those branches.
- In `fetchAllPages`, wrap the `fetcher` call in a `try/catch`; convert a caught `ParseError` to a `FetchError` by reading its `.message` and passing the original as `cause`.
Useful resources
Or clone locally
git clone -b challenge/2026-03-11 https://github.com/niltonheck/typedrop.git