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