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.

Or clone locally

git clone -b challenge/2026-05-17 https://github.com/niltonheck/typedrop.git