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.

Or clone locally

git clone -b challenge/2026-10-14 https://github.com/niltonheck/typedrop.git