Why Generate TypeScript Interfaces from JSON?
When you integrate a REST API, you receive JSON responses. Writing TypeScript interfaces by hand for every endpoint is tedious, error-prone, and inevitably gets out of sync as APIs evolve. An automatic generator reads the actual JSON response and produces accurate, named interfaces in seconds — saving hours across a large codebase.
How the Generator Works
The generator:
1. Parses the JSON value
2. Infers the TypeScript type for each key (string, number, boolean, null, nested interface, array)
3. Recursively processes nested objects — each becomes its own named interface
4. Inspects array contents — an array of strings becomes string[], array of objects becomes a named typed array
5. Outputs all interfaces in dependency order: leaf types first, root type last (or first, depending on style)
Complete Generated Output Example
Input JSON (real API response structure):
{
"user": {
"id": 1,
"name": "Ravi Mehta",
"email": "ravi@example.com",
"active": true,
"score": 98.5,
"roles": ["admin", "editor"],
"address": {
"city": "Surat",
"state": "Gujarat",
"pincode": "395001"
}
},
"meta": {
"requestId": "req_xyz123",
"timestamp": "2025-06-01T10:30:00Z"
}
}Generated TypeScript interfaces:
interface Address {
city: string;
state: string;
pincode: string;
}
interface User {
id: number;
name: string;
email: string;
active: boolean;
score: number;
roles: string[];
address: Address;
}
interface Meta {
requestId: string;
timestamp: string;
}
interface Root {
user: User;
meta: Meta;
}JSON to TypeScript Type Mapping
| JSON value | TypeScript type | Notes |
|---|---|---|
"hello" | string | |
42, 3.14 | number | Both int and float → number |
true, false | boolean | |
null | null | Widen to T | null manually |
[] | unknown[] | No element type info |
["a","b"] | string[] | |
[1,"x"] | (number | string)[] | Union type |
{} | Named interface | Recursive |
interface vs type — Which to Use?
// interface — preferred for object shapes; supports declaration merging
interface User {
id: number;
name: string;
}
// type alias — preferred for unions, primitives, and computed types
type UserId = number;
type Status = "active" | "inactive" | "pending";
type ApiResponse<T> = { data: T; meta: Meta };For API response types, use interface for objects. Use type for union types, string enums, and generic wrappers.
Optional Fields and Nullable Types
The generator creates required fields by default. After generation, add ? for optional fields and | null for nullable fields based on the real API contract:
interface User {
id: number;
name: string;
email: string;
middleName?: string; // optional — may be absent
bio: string | null; // nullable — present but may be null
deletedAt: string | null; // nullable timestamp
avatarUrl?: string | null; // optional AND nullable
}Generic Wrapper for API Responses
Most APIs wrap responses in a consistent envelope. Create a generic wrapper once:
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
requestId: string;
}
interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
totalPages: number;
};
}
// Usage:
type UserResponse = ApiResponse<User>;
type UserListResponse = PaginatedResponse<User>;Type Guards for Runtime Safety
TypeScript interfaces are erased at runtime — they provide no runtime protection. Add a type guard for untrusted data:
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof (obj as User).id === "number" &&
typeof (obj as User).name === "string" &&
typeof (obj as User).email === "string"
);
}
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const json: unknown = await response.json();
if (!isUser(json)) {
throw new Error("Unexpected API response shape");
}
return json; // now typed as User
}For more robust runtime validation, use Zod (see the JSON to Zod guide).
Using Generated Interfaces with fetch
// Place generated types in src/types/api.ts
import type { Root, User } from "./types/api";
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = (await response.json()) as Root;
return data.user;
}
// TypeScript now knows data.user.address.city is a string
const user = await getUser(42);
console.log(user.address.city.toUpperCase()); // fully typedBest Practices
- Use the actual API response as input, not a hand-crafted example — real data catches types you might miss
- Generate after every API contract change — keep types in sync automatically
- Store in a dedicated file —
src/types/api.tsor co-located with the API module - Review null fields — change
field: nulltofield: string | nullbased on the API spec - Add optional markers — fields absent in some responses should be
field?: type - Run tsc after generating — catch any type mismatches immediately
Use JSONKit's JSON to TypeScript tool to generate interfaces from any JSON response instantly — paste, generate, copy, paste into your codebase.