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