Here is something that happens constantly. You get a new API endpoint to integrate. The response JSON looks like this:
{
"id": 1847,
"user": {
"name": "Priya",
"email": "priya@example.com",
"role": "admin",
"preferences": {
"theme": "dark",
"notifications": true
}
},
"orders": [
{
"orderId": "ORD-991",
"total": 129.99,
"status": "shipped",
"items": [
{ "sku": "A12", "qty": 2 }
]
}
],
"createdAt": "2024-03-10T11:30:00Z"
}
And you start writing:
interface Preferences {
theme: string;
notifications: boolean;
}
interface User {
name: string;
email: string;
role: string;
preferences: Preferences;
}
// ...20 more minutes of this
It is tedious. It is where typos live. And when the API changes, you have to hunt through the interfaces to find what's outdated.
Generate them instead
Tools that infer TypeScript interfaces from a JSON sample have been around for a while. Paste the response, get the interfaces. Ten seconds instead of twenty minutes.
JSON to TypeScript does this in the browser. Paste JSON, click Generate, copy the output. The whole response above becomes:
export interface Root {
id: number;
user: User;
orders: Order[];
createdAt: string;
}
export interface User {
name: string;
email: string;
role: string;
preferences: Preferences;
}
export interface Preferences {
theme: string;
notifications: boolean;
}
export interface Order {
orderId: string;
total: number;
status: string;
items: Item[];
}
export interface Item {
sku: string;
qty: number;
}
Nested objects get their own named interfaces. Arrays are typed correctly. The root interface name defaults to Root but you can set it to ApiResponse or whatever fits your codebase.
What to review after generating
The generated types are inferred from your sample, not from the full API contract. A few things worth checking before you commit them:
Optional fields. If a field might be absent in some responses, add ?. The generator can only see what's in the sample you gave it.
// Generated
email: string;
// More accurate if email can be missing
email?: string;
Union types. A status field will be typed as string even if the API only ever returns three specific values. Narrow it if you want exhaustiveness checking:
// Generated
status: string;
// Better for switch statements and type guards
status: "shipped" | "pending" | "cancelled";
Dates. JSON has no date type, so createdAt comes through as string. If you parse it into a Date object before using it, update the interface to match what your code actually works with downstream.
Nullable fields. If a field is null in your sample, it types as null. In practice it might be string | null, so check the API docs and update accordingly.
A workflow that holds up
- Grab a real response from the API using DevTools, curl, or Postman.
- Paste it into the generator and copy the output.
- Adjust optional fields and union types against the API docs.
- Drop the interfaces into your project and use them in your fetch code.
const res = await fetch('/api/session');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json() as Root;
// data.user.preferences.theme is now typed
The interfaces double as documentation for what the API returns. When the API changes and you update the interfaces, TypeScript surfaces every place in your codebase that's now broken.
Works well for
- REST API responses
- GraphQL query results you've run in a playground
- localStorage data with a known shape
- Config files loaded at runtime
- Mock data in tests
Hand-writing interfaces is fine for small flat objects. For anything with nested levels, generation is faster and you get better coverage than most people write by hand. Treat the output as a starting point, make the optional and union type adjustments, and move on.
Top comments (0)