How to Type an API Response in TypeScript

fetch gives you data typed as any, which switches TypeScript off exactly where bugs come from. Here is how to give API data a real type, quickly and safely.

TypeScript checks your code, but it cannot check data that arrives at runtime. The moment you call await response.json(), you get a value of type any, and from then on the compiler will let you read user.adress.city with a typo and no complaint. Typing API responses closes that gap. This guide shows how to write the types quickly from a sample, how to model the tricky parts, and when a type alone is not enough.

Step 1: get the types from a real sample

Writing interfaces by hand for a large response is slow and error-prone. Copy a real response instead, for example from the browser's Network tab or from the API documentation, and paste it into JSON to TypeScript. A response like this:

{
  "id": 1024,
  "name": "Ada",
  "tags": ["admin", "beta"],
  "address": { "city": "London", "zip": "N1 9GU" },
  "orders": [
    { "order_id": "A-17", "total": 42, "shipped_at": "2026-09-01T10:00:00Z" },
    { "order_id": "A-18", "total": 18.5, "shipped_at": null, "coupon": "WELCOME" }
  ]
}

becomes, with the Root name set to User:

export interface Address {
  city: string;
  zip: string;
}

export interface Order {
  order_id: string;
  total: number;
  shipped_at: string | null;
  coupon?: string;
}

export interface User {
  id: number;
  name: string;
  tags: string[];
  address: Address;
  orders: Order[];
}

Notice what the generator worked out from the two orders: shipped_at was null in one item, so it is string | null; coupon was missing from one item, so it is optional. Nested objects became their own named interfaces, and the items of orders became Order. Everything runs in the browser, so a response with real customer data is not sent anywhere.

Step 2: use the type with fetch

The simplest way to connect the type to the data is a small helper:

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

const user = await getJson<User>('/api/users/1024');
user.address.city;   // string, checked by the compiler

The as T is a promise you make to the compiler, not a check. If the server sends something else, TypeScript will not notice. For internal APIs you control, that trade-off is often fine; for anything else, read on to step 4.

Step 3: model the tricky parts

In the JSONIn TypeScriptWhy
A key that is sometimes missingcoupon?: stringOptional: the property may not exist
A value that is sometimes nullshipped_at: string | nullNullable: the property exists but can be empty
Bothnote?: string | nullThe API may omit it or send null
A datestringJSON has no date type; convert with new Date() where needed
A large IDstring if the API sends it quotedNumbers above 2^53 lose precision in JavaScript
An empty array in the sampleunknown[]The sample does not show what the items look like

Prefer unknown to any for anything the sample cannot tell you: unknown forces a check before use, while any silently turns checking off. A generated type is only as complete as its sample, so use a response that includes the optional fields, or an array with several varied items.

Step 4: validate at runtime when the data is not yours

Third-party APIs change, proxies return HTML error pages, and old clients meet new servers. A runtime check catches those cases at the boundary, with a clear error, instead of deep in the interface. Two common approaches:

  • JSON Schema: generate a schema from the same sample with the JSON Schema generator and validate with a library such as Ajv. The schema is language-neutral, so a backend in another language can use the same file.
  • A TypeScript-first validator: libraries such as Zod let you describe the shape once and infer the TypeScript type from it, so the type and the check can never drift apart.

Either way, validate once, where the data enters your program, and let the rest of the code rely on the type.

When the API changes

Types generated once drift as the API grows. Keep the sample you generated from in the repository next to the types, so the next person can regenerate them and see what changed in a diff. If the API publishes an OpenAPI description, generating types from that is more reliable than from samples, because it lists every optional field and every possible value, including the ones a single response never shows. For APIs without one, a runtime validator from step 4 turns a silent mismatch into an error you can see in the logs.

interface or type?

For API data, both work. Interfaces can be extended and are merged when declared twice, which some teams like for public types; type aliases can also express unions and mapped types. Pick one style for the project and stay consistent. The generator produces either.

Other languages from the same sample

The same JSON sample can also become Go structs, Pydantic models or C# classes in JSON to Types, which helps keep a frontend and a backend on the same shape. If the sample itself is broken, the JSON formatter shows where.

Frequently Asked Questions

How do I generate TypeScript types from JSON?

Paste a real sample of the JSON into a JSON to TypeScript converter. It creates interfaces for every object, with optional and nullable fields detected from the sample.

Why is res.json() typed as any?

Because TypeScript cannot know at compile time what a server will send. You give the result a type yourself, with a generic helper or a validator.

Does a TypeScript type validate the data?

No. Types are removed when the code is compiled. To check data at runtime, use a JSON Schema validator or a library such as Zod.

Should I use optional or null?

Match the API: use a question mark when the key can be missing, and a null union when the key is present with a null value.

How should dates in JSON be typed?

As string, since JSON has no date type. Convert them to Date objects where you need date arithmetic.