TypeDrop
2026-10-14 Challenge
2026-10-14
Medium
Typed Paginated API Client with Aggregation
You're building the data-access layer for an analytics dashboard that pulls records from a paginated REST API. The client must handle cursor-based pagination transparently, aggregate results in a single pass, and surface typed summaries — the compiler must enforce that every page shape, cursor token, and aggregation result matches the schema declared up front.
Goals
- Declare a phantom `Brand<B,T>` helper and use it to create the opaque `SaleId` type.
- Implement `fetchAllPages<T>` to follow a cursor chain across an arbitrary number of pages without using `any`.
- Implement `paginatedAggregate<T, S>` that folds each page's items through an `Aggregator` in a single streaming pass — no full buffering.
- Implement `buildSaleSummaryAggregator` returning a fully-typed `Aggregator<SaleRecord, SaleSummary>` that computes total amount, per-region counts, and the largest sale.
challenge.ts
// Key types and main function signatures
type Brand<B, T> = T & { readonly __brand: B };
type SaleId = Brand<"SaleId", string>;
type SaleRecord = {
id: SaleId;
region: "NA" | "EU" | "APAC";
amount: number;
currency: "USD" | "EUR" | "JPY";
};
type Page<T> = { items: T[]; nextCursor?: string; totalCount: number };
type FetchPage<T> = (cursor: string | undefined) => Promise<Page<T>>;
interface Aggregator<T, S> {
init(): S;
reduce(acc: S, item: T): S;
finalize(acc: S): S;
}
// --- functions you must implement ---
async function fetchAllPages<T>(fetchPage: FetchPage<T>): Promise<T[]> { ... }
async function paginatedAggregate<T, S>(
fetchPage: FetchPage<T>,
aggregator: Aggregator<T, S>
): Promise<S> { ... }
function buildSaleSummaryAggregator(): Aggregator<SaleRecord, SaleSummary> { ... }
Hints (click to reveal)
Hints
- For `Brand<B,T>`, intersect `T` with `{ readonly __brand: B }` — the phantom property is never set at runtime but blocks accidental structural compatibility.
- In `fetchAllPages` and `paginatedAggregate`, keep a `cursor: string | undefined` variable initialised to `undefined` and loop until the returned page has no `nextCursor`.
- Use `SaleRecord["region"]` as the key type for `countByRegion` so the `Record` stays in sync with the union automatically — no manual duplication needed.
Useful resources
Or clone locally
git clone -b challenge/2026-10-14 https://github.com/niltonheck/typedrop.git