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