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:
{
"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:
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.
addressturns intoAddress, so you can reuse the type elsewhere. - Array items get a singular name.
ordersbecomesOrder[]. - 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
nullsays nothing about the type. The sample shows thatemailcan be null, but not what it is when it has a value, so the result isunknown | null. Replaceunknownwithstringonce you know.
interface or type?
For a plain object shape, both work and are interchangeable in most code:
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:
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 havecoupon, 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"becomesstring. 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.
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
unknownin place. Replace it as soon as you know the real type. - Typing dates as
Date.JSON.parsenever createsDateobjects; the value is a string until you convert it. - Shadowing built-in names. Avoid naming your types
Response,ErrororEvent. - Using
asinstead 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
Base64 is not encryption — what it is, when to use it and the URL-safe variant
How Base64 turns bytes into text, why it hides nothing, how to handle accents and emoji correctly and when you need the URL-safe alphabet.
November 5, 20265 min readTool: Base64 Encoder/Decoder
The regex that freezes the browser — catastrophic backtracking explained
Why a short regular expression can take seconds or hang a page, how to recognize nested quantifiers and how to rewrite the pattern so it stays fast.
November 5, 20266 min readTool: Regex Tester