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.
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.
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.
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:
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.
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.
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:
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 stringThe 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.
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.
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:
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
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.
Frequently Asked Questions
What type does fetch return in TypeScript?
How do I type the result of response.json()?
Does fetch throw on HTTP errors like 404?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.