TypeDrop

2026-03-30 Challenge

2026-03-30 Medium

Typed API Response Paginator

You're building the data-fetching layer for an analytics dashboard that consumes a paginated REST API. The client must fetch pages sequentially or in parallel up to a concurrency limit, validate each raw response at runtime, and aggregate all records into a typed `Result<T, E>` — with clean handling for partial failures.

Goals

  • Implement `validateRecord` and `validateEnvelope` to narrow `Record<string, unknown>` into fully typed domain objects, returning a `Result<T, string>` on the first failing field.
  • Implement `withConcurrency<T>` to run an array of async tasks with at most `limit` concurrent executions while preserving the original result order.
  • Implement `paginateAll` to fetch all pages via the injected `FetchPage`, using `withConcurrency` to respect the concurrency limit, and return a success or partial-failure `Result`.
  • Ensure the final success `value` (and the partial `records`) are sorted ascending by `timestamp` string.
challenge.ts
// Key types & main function signature

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

export type PaginatorError =
  | { kind: "validation"; page: number; reason: string }
  | { kind: "fetch";      page: number; reason: string }
  | { kind: "partial";   errors: PaginatorError[]; records: AnalyticsRecord[] };

export interface PaginatorConfig {
  totalPages:  number;
  concurrency: number; // max in-flight fetches
}

/** Injected fetcher — never throws, returns Result. */
export type FetchPage = (page: number) => Promise<Result<RawEnvelope, string>>;

/** Concurrency-limited task runner — preserves result order. */
export async function withConcurrency<T>(
  tasks: ReadonlyArray<() => Promise<T>>,
  limit: number
): Promise<T[]> { /* TODO */ }

/** Fetch, validate, and aggregate all pages into a single Result. */
export async function paginateAll(
  fetchPage: FetchPage,
  config: PaginatorConfig
): Promise<Result<AnalyticsRecord[], PaginatorError>> { /* TODO */ }
Hints (click to reveal)

Hints

  • For `withConcurrency`, consider processing tasks in fixed-size batches using `Promise.all` — slice the task array into chunks of size `limit` and await each chunk sequentially.
  • In `validateRecord`, use `typeof` guards and `Array.isArray` to narrow each field from `unknown` before assigning — the compiler will reject assignments without explicit narrowing.
  • In `paginateAll`, collect both errors and valid records in the same pass so you can return a `{ kind: 'partial' }` error that still carries the successfully parsed records (R4e).

Or clone locally

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