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-07-28 Easy

Typed HTTP API Client Builder

You're building the typed HTTP client layer for a mobile app's backend SDK. Consumers should be able to declare their API endpoints once — including method, path params, query params, and response shape — and get back a fully-typed `fetch` wrapper with zero `any` or unsafe casts.

Goals

  • Implement `ExtractPathParams<T>` as a recursive template-literal conditional type that unions all `:param` names found in a path string.
  • Implement `CallOptions<TPath>` as a conditional type that requires `pathParams: Record<ExtractPathParams<TPath>, string>` only when path params exist.
  • Implement the curried `defineEndpoint` factory so the first call infers `TPath` from the path argument and the second (empty) call pins `TResponse` via an explicit type parameter.
  • Implement `buildCaller` and `createApiClient` with correct runtime behaviour — path interpolation, query string appending, error and network-failure handling — all without losing the inferred response type.
challenge.ts
// Key types at a glance

type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

type EndpointDef<TPath extends string, TResponse> = {
  method: HttpMethod;
  path: TPath;
  _response?: TResponse; // phantom field — carries TResponse into the type system
};

// Extracts ":id" | ":postId" names from a path string at the type level
type ExtractPathParams<T extends string> = /* your recursive conditional type */;

// Requires `pathParams` only when the path actually has segments
type CallOptions<TPath extends string> = /* conditional type */;

type ApiResult<TResponse> =
  | { ok: true;  status: number; data: TResponse }
  | { ok: false; status: number; error: string   };

// Curried: first call infers TPath, second call pins TResponse
declare function defineEndpoint<TPath extends string>(
  method: HttpMethod,
  path: TPath
): <TResponse>() => EndpointDef<TPath, TResponse>;

// Usage:
const getUser = defineEndpoint("GET", "/users/:id")<User>();
const caller  = buildCaller(getUser, "https://api.example.com");
const result  = await caller({ pathParams: { id: "42" } }); // fully typed!
Hints (click to reveal)

Hints

  • For `ExtractPathParams`, match the pattern `` `${string}/:${infer Param}/${infer Rest}` `` first (segment in the middle), then `` `${string}/:${infer Param}` `` (segment at the end) — order matters in conditional types.
  • To make `pathParams` conditionally required, wrap the check as `[ExtractPathParams<TPath>] extends [never]` (tuple wrapping avoids distributivity issues).
  • For the curried `defineEndpoint`, the outer function returns `<TResponse>() => EndpointDef<TPath, TResponse>` — the caller writes `defineEndpoint('GET', '/users/:id')<User>()` to fix the response type without passing a runtime value.

Or clone locally

git clone -b challenge/2026-07-28 https://github.com/niltonheck/typedrop.git