TypeDrop
2026-05-17 Challenge
2026-05-17
Medium
Typed Paginated API Client with Result Handling
You're building the data-access layer for a developer dashboard that fetches issues from a project-management API. Responses arrive as `unknown` from a generic HTTP transport; your client must validate each page, accumulate results across pages, and return a strongly-typed aggregation report — with zero `any`.
Goals
- Implement `isIssueStatus` and `isIssuePriority` as type-predicate guards that narrow `unknown` to the correct union literal.
- Implement `parseIssue` and `parsePage` to validate raw `unknown` payloads and return a typed `Result<T, FetchError>` — no `any` or `as` casts.
- Implement `fetchAllIssues` to fetch page 1 first, then remaining pages in parallel with `Promise.all`, gracefully capturing both network rejections and validation failures into `failedPages`.
- Build the final `IssueReport` with all `byStatus` and `byPriority` keys guaranteed present (even if empty), and `failedPages` sorted in ascending page order.
challenge.ts
// Core Result type + domain types + main function signature
type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
type FetchError =
| { kind: "network"; message: string }
| { kind: "parse"; message: string; raw: unknown }
| { kind: "validation"; message: string; field: string };
interface Issue {
id: number; title: string;
status: IssueStatus; priority: IssuePriority;
assignee: string | null; createdAt: string;
}
interface IssueReport {
totalFetched: number;
byStatus: Record<IssueStatus, Issue[]>;
byPriority: Record<IssuePriority, Issue[]>;
unassignedCount: number;
failedPages: Array<{ page: number; error: FetchError }>;
}
type PageFetcher = (page: number) => Promise<unknown>;
async function fetchAllIssues(fetcher: PageFetcher): Promise<IssueReport> { /* TODO */ }
Hints (click to reveal)
Hints
- Use `typeof x === 'string' && (x === 'open' || ...)` patterns inside your type predicates — TypeScript will narrow the return type automatically.
- When validating an `unknown` object, first check `typeof raw === 'object' && raw !== null`, then use `'fieldName' in raw` before indexing — this avoids any cast.
- Wrap each `fetcher(page)` call in a `Promise.allSettled`-style `.then(...).catch(...)` chain so rejections become `{ kind: 'network' }` errors rather than unhandled exceptions.
Useful resources
Or clone locally
git clone -b challenge/2026-05-17 https://github.com/niltonheck/typedrop.git