How a JSON object maps to a TypeScript interface
Every key in a JSON object becomes one property in the interface, and each value picks the property's type: a JSON string becomes string, a number becomes number, and true/false become boolean. The generator walks the keys once, so a flat object converts in a single pass.
{
"id": 42,
"username": "alice",
"active": true
}interface Root {
id: number;
username: string;
active: boolean;
}Required, optional, and nullable fields
From a single JSON sample the generator cannot know which keys are optional — it only sees what is present, so every key is emitted as required. A value that is present butnull is a different case: it becomes a union with null, defaulting tostring | null because the real type is unknowable from null alone. Add the ? modifier yourself for keys the API may omit entirely.
{
"userId": 1,
"deletedAt": null
}interface Root {
userId: number;
deletedAt: string | null; // present but null -> union with null
bio?: string; // you add ? by hand: key may be absent
}Nested objects become nested interfaces
A nested object is given its own named interface that the parent references by name, instead of being inlined or typed as any. This keeps the output readable and lets you reuse a shape like Address elsewhere. The generator recurses to any depth, emitting one interface per distinct object shape.
{
"user": { "id": 1, "name": "Alice" },
"address": { "city": "Springfield", "zip": "62701" }
}interface Root {
user: User;
address: Address;
}
interface User {
id: number;
name: string;
}
interface Address {
city: string;
zip: string;
}Array type inference and union types
Arrays of one primitive type become typed arrays (string[], number[]). An array of objects becomes an array of a named interface, with fields merged across every item so a key missing from some items is marked optional. Mixed-type arrays produce a union array rather than a lazy any[].
{
"tags": ["dev", "premium"],
"scores": [98, 87],
"mixed": [1, "two", true],
"items": [
{ "id": 1, "name": "Widget A" },
{ "id": 2, "name": "Widget B", "onSale": true }
]
}interface Root {
tags: string[];
scores: number[];
mixed: (number | string | boolean)[];
items: Item[];
}
interface Item {
id: number;
name: string;
onSale?: boolean; // present on only some items -> optional
}Generator vs. hand-written: quicktype and json-to-ts
Reach for a generator — this browser tool, quicktype, or the json-to-tsnpm package — whenever a payload is large, deeply nested, or changes often. quicktypegoes further than a single-sample tool: it infers nullable vs. optional from multipleJSON samples and can emit runtime validators next to the types.
# quicktype CLI — multiple samples sharpen nullable/optional inference
npm install -g quicktype
quicktype --lang typescript --out User.ts user1.json user2.json// json-to-ts — programmatic, no runtime code
import jsonToTs from 'json-to-ts'
jsonToTs({ id: 1, name: 'Alice' }).forEach(i => console.log(i))Write interfaces by hand only for small, stable shapes, or when you want precise literal unions a generator cannot infer — for example role: "admin" | "user" instead ofrole: string. For a deeper walkthrough see the JSON to TypeScript guide.
type vs interface, and runtime validation
Use interface for plain object shapes from JSON — it is the conventional default and can be extended. Switch to type when you need a union, tuple, or mapped type that an interface cannot express. Either way, remember that interfaces are erased at compile time and validate nothing at runtime; JSON.parse returns any and skips every check. Pair the generated interface with a validator to guarantee the shape.
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
name: z.string(),
deletedAt: z.string().nullable(),
})
type User = z.infer<typeof UserSchema> // same as the interface
const user = UserSchema.parse(JSON.parse(raw)) // throws on a bad shapeTo go straight to a schema instead of an interface, try JSON to Zod. For typed Python models from the same JSON, see JSON to Pydantic.