Type Fetch in the Browser with TypeScript

Learn how to type fetch API responses in TypeScript. Handle Response, json(), error states, and typed API wrappers for safe browser requests.

7 min read

The fetch API is the modern way to make HTTP requests from the browser. TypeScript knows the shape of the Response object and its methods, but it cannot know the shape of the JSON data your API returns. You bridge that gap with type annotations and runtime checks.

How fetch is typed

The global fetch function returns a Promise that resolves to a Response object. TypeScript types both the function signature and the Response interface so you get autocomplete on status, ok, and the body-parsing methods.

typescripttypescript
async function loadData() {
  const response = await fetch('https://api.example.com/users');
  //    ^? const response: Response
 
  console.log(response.status); // e.g., 200
  console.log(response.ok);     // true if status is 200-299
}

The Response object is always typed the same way regardless of what the API returns. The status and ok properties tell you whether the request succeeded at the HTTP level, but they say nothing about the shape of the response body.

Typing the JSON response

response.json() returns Promise of any because TypeScript cannot know what JSON the server will send. You supply the type by annotating the variable or using a type assertion on the parsed result.

typescripttypescript
interface User {
  id: number;
  name: string;
  email: string;
}

The User interface describes the shape you expect the API to return. Pass it to the function that fetches and parses the data:

typescripttypescript
async function getUser(id: number): Promise<User> {
  const response = await fetch(`https://api.example.com/users/${id}`);
  if (!response.ok) {
    throw new Error(`HTTP error: ${response.status}`);
  }
 
  const user: User = await response.json();
  return user;
}

The type annotation on user tells TypeScript that you expect the JSON to match the User interface. This is a compile-time promise, not a runtime guarantee. If the API changes its response shape, you will only find out at runtime when you try to access a missing property.

Handling errors correctly

fetch rejects its promise only on network errors like a DNS failure or a disconnected internet connection. HTTP errors like 404 or 500 do not cause rejection. You must check response.ok yourself and throw on failure.

typescripttypescript
async function fetchWithError(url: string) {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error(`Request failed with status ${response.status}`);
    return await response.json();
  } catch (error) {
    if (error instanceof TypeError) console.log('Network error:', error.message);
    else if (error instanceof Error) console.log('HTTP or parsing error:', error.message);
    return null;
  }
}

The catch block receives unknown because anything can be thrown in JavaScript. Narrow with instanceof to handle network errors (TypeError) and HTTP errors (Error) separately. Returning null lets the caller check whether the fetch succeeded.

A typed fetch wrapper

Instead of repeating null checks and error handling in every function, create a small typed wrapper. This keeps the generic type and the error handling in one place.

typescripttypescript
async function apiGet<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  return response.json() as Promise<T>;
}

The generic type parameter T flows from the call site through to the return type. The caller writes the type once and gets a fully typed result. The wrapper handles the HTTP status check and the JSON parsing:

typescripttypescript
interface Product {
  id: number;
  name: string;
  price: number;
}
 
const product = await apiGet<Product>('https://api.example.com/products/1');
console.log(product.name); // OK, typed as string

The fetch flow

Understanding the fetch lifecycle helps you know where to add types. The browser handles DNS, connection, and HTTP at the network level. TypeScript helps you at the JavaScript level with the Response object.

Fetch request and response typing flow

The Response object arrives with status and headers regardless of success. You branch on ok to decide whether to parse the body or handle the error. The parsed data gets your type annotation at the point where you call response.json.

Sending data with POST

When you send data to an API, TypeScript types the request body through the fetch options. The body option accepts several types including string, FormData, and Blob.

typescripttypescript
interface CreateUser {
  name: string;
  email: string;
}

The CreateUser interface types the data you send, separately from the User interface that types the response you get back. Pass it into a function that posts the data and parses the result:

typescripttypescript
async function createUser(data: CreateUser): Promise<User> {
  const response = await fetch('https://api.example.com/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
 
  if (!response.ok) {
    throw new Error(`Failed to create user: ${response.status}`);
  }
 
  return response.json() as Promise<User>;
}

JSON.stringify accepts the typed CreateUser object and produces the string body that fetch expects. The headers object is typed as HeadersInit, which accepts plain objects with string keys and values.

Common mistakes

Forgetting to check response.ok is the most common fetch mistake. TypeScript does not warn you about this because the Response type is the same regardless of the HTTP status. A 404 response still gives you a valid Response object with a .json() method, and calling it on an error response may throw a different parsing error that masks the real problem.

Using response.json() without a type annotation leaves the result as any. This disables type checking on the data and lets property access errors slip through to runtime. Always add a type annotation or a generic to tell TypeScript what shape you expect.

Not catching both network errors and HTTP errors separately leads to confusing error messages. A TypeError from a network failure and an Error from a 500 status need different handling. Narrow the error in the catch block to respond appropriately.

See Type Event Handlers in TypeScript for typing the events that trigger fetch calls. See Type Local Storage in TypeScript for caching fetched data in the browser.

Rune AI

Rune AI

Key Insights

  • fetch returns Promise of Response, not the parsed data directly.
  • response.json() returns Promise of any, so add a type annotation on the result.
  • Always check response.ok before parsing, because fetch does not throw on HTTP errors.
  • Handle network errors and HTTP errors in separate catch or if blocks.
  • A typed fetch wrapper with a generic reduces repetition and keeps error handling central.
RunePowered by Rune AI

Frequently Asked Questions

What type does fetch return in TypeScript?

fetch returns Promise of Response. The Response object has ok, status, and methods like json and text that also return Promises. You need to type the parsed data yourself because TypeScript cannot know the shape of the API response.

How do I type the result of response.json()?

response.json() returns Promise of any in TypeScript. You add a type annotation on the result or use a generic type assertion to tell TypeScript the expected shape. The typing is only at compile time, so validate the data at runtime if the API shape is unreliable.

Does fetch throw on HTTP errors like 404?

No. fetch only rejects on network errors, not on HTTP error status codes. You must check response.ok or response.status yourself and throw if the status indicates failure.

Conclusion

TypeScript types fetch at the Response level but cannot know what JSON shape your API returns. Use a generic to annotate the expected type, check response.ok before parsing, handle both network errors and HTTP errors in catch blocks, and avoid the any return from response.json by adding a type assertion or a typed wrapper function.