TypeDrop

2026-09-30 Challenge

2026-09-30 Hard

Typed Concurrent API Client with Retry, Cancellation & Result Aggregation

You're building the data-fetching layer for a distributed dashboard that fans out requests to multiple third-party microservices simultaneously. Each endpoint has its own response shape, requests must be cancellable, failed requests are retried with exponential back-off, and the final aggregated report is a discriminated-union result per endpoint — all enforced by the compiler with zero `any`.

Goals

  • Define `Result<T,E>` and `FetchError` as discriminated unions, then derive `EndpointResponse` with `infer` and `AggregatedResults` with a mapped type.
  • Implement `fetchWithRetry` with abort-signal checking, parse-error short-circuiting, and exponential back-off retries gated by a `shouldRetry` predicate.
  • Implement `fetchAll` that fans out all endpoints concurrently via `Promise.allSettled`, wires a single global `AbortController` timeout, and returns a fully-typed `AggregatedResults<M>`.
  • Implement the three result helpers — `isOk` (type guard), `unwrapOr` (with `F extends T`), and `mapResult` — without any unsafe casts.
challenge.ts
// Core types you must define and implement:

type Result<T, E> =
  | { status: "ok";  value: T }
  | { status: "err"; error: E };

type FetchError =
  | { kind: "network"; message: string }
  | { kind: "timeout"; timeoutMs: number }
  | { kind: "parse";   raw: string; cause: string };

interface Endpoint<T> {
  url: string;
  parse: (raw: unknown) => Result<T, Extract<FetchError, { kind: "parse" }>>;
  timeoutMs?: number;
  maxRetries?: number;
}

type EndpointMap = Record<string, Endpoint<unknown>>;

// Given Endpoint<T>, extract T:
type EndpointResponse<E> = /* TODO: use infer */ never;

// Maps each key of M to Result<its-response-type, FetchError>:
type AggregatedResults<M extends EndpointMap> = /* TODO: mapped type */ never;

// Main functions to implement:
async function fetchWithRetry<T>(
  endpoint: Endpoint<T>,
  rawJson: unknown,
  retryConfig: RetryConfig,
  signal: AbortSignal
): Promise<Result<T, FetchError>> { /* ... */ }

async function fetchAll<M extends EndpointMap>(
  endpoints: M,
  rawResponses: { [K in keyof M]: unknown },
  retryConfig: RetryConfig,
  timeoutMs?: number
): Promise<AggregatedResults<M>> { /* ... */ }
Hints (click to reveal)

Hints

  • For `EndpointResponse`, write `E extends Endpoint<infer T> ? T : never` — the compiler will unpack the generic parameter for you.
  • In `fetchAll`, `Promise.allSettled` returns `PromiseSettledResult<Result<T, FetchError>>[]`; a `.rejected` settlement (which shouldn't normally happen) can be mapped to a `{ kind: 'network', message: ... }` err.
  • To build `AggregatedResults<M>` at runtime inside `fetchAll`, accumulate into a `Partial` record and cast with `as AggregatedResults<M>` only at the final return — that's the one place a cast is acceptable because you've verified all keys.

Or clone locally

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