TypeDrop

A new TypeScript challenge every day. Sharpen your types.

TypeDrop delivers a fresh TypeScript challenge every day, generated by AI. Pick a challenge, open it in StackBlitz (preferred) or CodeSandbox (or clone it locally), and make the tests pass. No accounts, no setup — just you and the type system.

Learn more on GitHub →

2026-04-09 Medium

Typed API Pagination Crawler

You're building the data-sync layer for a SaaS dashboard that must pull all records from a paginated REST API. Pages arrive as unknown JSON; your crawler must validate each page, fetch all pages concurrently up to a limit, and aggregate the results into a fully typed report — with zero `any`.

Goals

  • Implement `isProductCategory`, `validateProduct`, and `validateRawPage` as strict runtime type guards that narrow `unknown` to typed domain values.
  • Implement `withConcurrencyLimit<T>` to run async tasks in parallel up to a fixed concurrency cap while preserving result order.
  • Implement `crawlAllPages` to discover total pages from page 1, fetch remaining pages concurrently, and aggregate all valid products into a `CrawlReport` keyed by all four `ProductCategory` values.
  • Surface every failure — network rejections and page validation errors — as typed `CrawlError` entries inside `CrawlReport.errors`, never as thrown exceptions.
challenge.ts
// Key types & main function signature

export type ProductCategory = "electronics" | "clothing" | "food" | "books";

export interface Product {
  id: string; name: string;
  category: ProductCategory; priceUsd: number; inStock: boolean;
}

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

export type CrawlError =
  | { kind: "network";    message: string }
  | { kind: "validation"; page: number; message: string }
  | { kind: "timeout";    page: number };

export type CategoryReport = Record<ProductCategory, CategoryStats>;

export interface CrawlReport {
  totalProducts: number; totalPages: number;
  byCategory: CategoryReport; errors: CrawlError[];
}

export type PageFetcher = (page: number) => Promise<unknown>;

export async function crawlAllPages(
  fetcher: PageFetcher,
  concurrencyLimit: number = 3
): Promise<Result<CrawlReport, CrawlError>> { /* TODO */ }
Hints (click to reveal)

Hints

  • A `Record<ProductCategory, CategoryStats>` must include all four keys — initialise them all to `{ count: 0, totalValueUsd: 0, inStockCount: 0 }` before iterating products.
  • For `withConcurrencyLimit`, maintain a running count of active promises and a queue index; use a recursive 'slot' pattern rather than `Promise.all` on all tasks at once.
  • In `validateProduct` and `validateRawPage`, narrow `unknown` step-by-step with `typeof` and `Array.isArray` checks — the compiler will track each narrowing for you without any assertions.

Or clone locally

git clone -b challenge/2026-04-09 https://github.com/niltonheck/typedrop.git