Advanced TypeScript Mapping: Safely Typing Dynamic API Responses Without any

Every API client eventually hits the same wall: the response shape isn't known until runtime, so the temptation is to slap any on it and move on. That works right up until a renamed field or a null-where-you-expected-a-string ships to production undetected.

Mapped types solve this without runtime overhead. Instead of describing what a type is, they describe how to transform one type into another — which is exactly what an API boundary needs: raw JSON in, a safe, narrowed shape out.

The core idea: transform, don't guess

A mapped type iterates over the keys of an existing type and rebuilds it:

type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Partial<T> = {
  [K in keyof T]?: T[K];
};

Both ship in TypeScript's standard library, but the real power shows up when you combine mapped types with conditional types, key remapping (as), and template literal types to model something an API actually returns.

Step 1 — Model the response as a discriminated union

Don't type the payload. Type the state of the request:

type ApiResult<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: { code: string; message: string } }
  | { status: "loading" };

function handle<T>(result: ApiResult<T>) {
  if (result.status === "success") return result.data; // T — narrowed
  if (result.status === "error") return console.error(result.error.message);
  return null; // loading
}