TypeDrop
2026-05-24 Challenge
2026-05-24
Hard
Typed Paginated API Client with Cursor-Based Aggregation
You're building the data-ingestion layer for an analytics dashboard that pulls user activity events from a cursor-paginated REST API. Pages arrive as `unknown`; your client must validate each page, fan out concurrent fetches up to a concurrency limit, aggregate results through a typed single-pass reducer, and surface a strongly-typed report — with zero `any`.
Goals
- Implement branded constructors `makeCursor` and `makePageSize` that return `Result` types with precise error literals.
- Implement `validatePage` that narrows `unknown` API responses to `Page<ActivityEvent>` using only type guards — no type assertions.
- Implement `aggregateEvents` that computes a fully-typed `AggregationReport` in a single pass with an exhaustive `KindStats` mapped type.
- Implement `fetchAllPages` with a rolling concurrency window and `runIngestion` that wires validation, pagination, and aggregation into one `Result`-returning async pipeline.
challenge.ts
// Core types & main orchestrator signature at a glance
export type Cursor = string & { readonly __brand: "Cursor" };
export type PageSize = number & { readonly __brand: "PageSize" };
export type EventKind = "click" | "view" | "purchase" | "share";
export type Result<T, E extends string> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: E; readonly detail: string };
export interface Page<T> {
readonly items: readonly T[];
readonly nextCursor: Cursor | null;
readonly totalCount: number;
}
export type KindStats = {
readonly [K in EventKind]: {
readonly count: number;
readonly totalDurationMs: number;
readonly totalRevenueUsd: number;
};
};
export type PageFetcher =
(cursor: Cursor | null, pageSize: PageSize) => Promise<unknown>;
// Ties everything together — validate config, paginate, aggregate
export async function runIngestion(
fetcher: PageFetcher,
config: ClientConfig,
): Promise<Result<AggregationReport, "InvalidPageSize" | "FetchFailed">>;
Hints (click to reveal)
Hints
- For `validatePage`, build a series of narrowing helper functions (e.g. `isActivityEvent`) that check each field individually — TypeScript needs explicit `typeof` / `in` guards, not casts.
- For `KindStats`, initialise all four keys up front using an object literal that satisfies the mapped type — the compiler will catch any missing key at compile time.
- For the concurrency window in `fetchAllPages`, think of a recursive "slot" pattern: start up to `concurrency` fetches, and as each resolves, immediately launch the next cursor if one is available — rather than batching pages in fixed groups.
Useful resources
Or clone locally
git clone -b challenge/2026-05-24 https://github.com/niltonheck/typedrop.git