Skip to content

interface or type? Generating TypeScript types from JSON

How to turn a JSON response into TypeScript types, when to choose interface or type, and what a type inferred from one sample can and cannot promise.

By Felipe YamaokaPublished November 5, 20265 min read

Typing an API you did not write

You call an endpoint, get a JSON response back and want autocompletion and type checking for it. The API has no OpenAPI file and no SDK, only an example in the documentation, or the response you just saw in the network tab.

Writing the types by hand is slow and error-prone, especially with nested objects and arrays. Generating them from a sample is fast, but you need to know what the generator assumes and which decisions are still yours. This guide covers both, plus the eternal question: interface or type?

From a sample to types

Take this response from a fictional users API:

json
{
  "id": 42,
  "name": "Ada",
  "email": null,
  "tags": ["admin", "beta"],
  "address": { "city": "São Paulo", "zip": "01000-000" },
  "orders": [
    { "id": 1, "total": 99.9, "coupon": "BLACK10" },
    { "id": 2, "total": 15 }
  ],
  "created-at": "2026-11-05T12:00:00Z"
}

Pasting it into the JSON to TypeScript converter with the root name User gives:

ts
interface User {
  id: number;
  name: string;
  email: unknown | null;
  tags: string[];
  address: Address;
  orders: Order[];
  "created-at": string;
}

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

interface Order {
  id: number;
  total: number;
  coupon?: string;
}

Several rules are at work here, and they are the same ones you would apply by hand:

  • Nested objects become named declarations. address turns into Address, so you can reuse the type elsewhere.
  • Array items get a singular name. orders becomes Order[].
  • Objects inside an array are merged. The second order has no coupon, so the field becomes optional: coupon?: string.
  • Keys that are not valid identifiers stay quoted. "created-at" cannot be written as a bare property name.
  • A lone null says nothing about the type. The sample shows that email can be null, but not what it is when it has a value, so the result is unknown | null. Replace unknown with string once you know.

interface or type?

For a plain object shape, both work and are interchangeable in most code:

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

type Address = {
  city: string;
  zip: string;
};

The differences show up at the edges.

What only type can do. A type alias can name anything, not just objects: unions (type Status = "paid" | "pending"), tuples (type Point = [number, number]), primitives, function types and mapped or conditional types. When the shape of your data is "one of these", you need type.

What only interface can do. Interfaces support declaration merging: two interface Window declarations in scope are combined into one. Libraries use this to let you add properties to their types. It is also a risk: if a generated interface Response lands in a global script, it merges with the DOM's Response instead of creating a new type. That is why the converter on this site renames such roots, for example to Response2, instead of shadowing a built-in type.

Extending. Interfaces use extends, types use intersections:

ts
interface Admin extends User {
  permissions: string[];
}

type Admin2 = User & { permissions: string[] };

The TypeScript team recommends extends for object types when you build large hierarchies: the compiler caches interface relationships, and errors on conflicting properties are clearer than the never you sometimes get from an intersection.

A practical rule. Use interface for object shapes that other code may extend, such as API models. Use type for unions, tuples and anything computed from other types. Pick one style for plain objects in a project and stay consistent; the converter lets you generate either.

What a sample cannot tell you

A type inferred from one response describes that response, not the API contract. Keep these limits in mind:

  • Missing fields look optional or do not exist. If the sample never shows phone, the type will not have it. If only some items have coupon, it is optional, which may or may not be true for every response.
  • Dates are strings. JSON has no date type, so "2026-11-05T12:00:00Z" becomes string. Convert it explicitly when you read it.
  • All numbers are number. The sample cannot tell an integer ID from a price, and very large IDs may already have lost precision; in that case the API should send them as strings.
  • Unions need more samples. If a field is sometimes a number and sometimes a string, you only see it when both appear. Paste an array with several real responses to make the generator merge them.
  • Empty arrays are unknown[]. There is nothing to infer from [].

Types are not validation

TypeScript types disappear when the code is compiled. Writing const user = (await response.json()) as User does not check anything at runtime; if the API sends something else, the error shows up later, far from the cause.

For data that comes from outside your program, use the generated types as a starting point and validate at the boundary: a schema library such as Zod or Valibot, or a small hand-written check for the fields you depend on. The type then describes data you have actually verified.

ts
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error(`Request failed: ${response.status}`);
  const data: unknown = await response.json();
  if (typeof data !== "object" || data === null || !("id" in data))
    throw new Error("Unexpected response");
  return data as User;
}

Common mistakes

  • Trusting a single sample. Generate from several responses, including edge cases such as empty lists and null fields.
  • Leaving unknown in place. Replace it as soon as you know the real type.
  • Typing dates as Date. JSON.parse never creates Date objects; the value is a string until you convert it.
  • Shadowing built-in names. Avoid naming your types Response, Error or Event.
  • Using as instead of validating. A type assertion is a promise you make to the compiler, not a check.

Summary

Generating TypeScript types from JSON saves time and gets nested objects, arrays and optional fields right, as long as you remember the result describes the sample, not the contract. Prefer interface for object shapes that may be extended and type for unions, tuples and computed types. And validate data from outside before you trust its type.

Keep reading