Typing fetch Responses

response.json() is Not Typed

fetch returns Promise<Response>, and response.json() returns Promise<any> (or unknown, depending on the type definitions). TypeScript cannot know what the server sends. You decide the type, ideally after validating it.

type Product = { id: number; name: string; price: number };

async function getProducts(): Promise<Product[]> {
  const res = await fetch("https://api.shopease.in/products");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return (await res.json()) as Product[]; // trust the API (not checked at runtime)
}

A Typed fetch Helper (Simulated)

Example

The examples cannot use the network, so a fake fetch builds real Response objects. The getJson helper checks the HTTP status and runs a validator, so callers get a correctly typed value or an error.

type Product = { id: number; name: string; price: number };

async function fakeFetch(url: string): Promise<Response> {
  if (url.endsWith("/products/1")) {
    return new Response(JSON.stringify({ id: 1, name: "Kettle", price: 1299 }), { status: 200 });
  }
  return new Response(JSON.stringify({ message: "Not found" }), { status: 404 });
}

async function getJson<T>(url: string, isT: (x: unknown) => x is T): Promise<T> {
  const res = await fakeFetch(url);
  if (!res.ok) throw new Error(`GET ${url} failed with ${res.status}`);
  const data: unknown = await res.json();
  if (!isT(data)) throw new Error("Unexpected response shape");
  return data;
}

const isProduct = (x: unknown): x is Product =>
  typeof x === "object" && x !== null && "id" in x && "name" in x && "price" in x;

async function main() {
  const p = await getJson("/api/products/1", isProduct);
  console.log(p.name, p.price);
  try {
    await getJson("/api/products/99", isProduct);
  } catch (e) {
    console.log((e as Error).message);
  }
}
main();

Output

Kettle 1299
GET /api/products/99 failed with 404