Generating TypeScript Types from JSON: A Practical Guide
Hand-writing a TypeScript interface for a 40-field API response is tedious and error-prone — a typo in a field name silently breaks type safety instead of throwing a compile error. Generating the interface directly from a real JSON sample is faster and more accurate, as long as you understand where inference has to guess.
How JSON-to-TypeScript inference works
A generator walks the JSON value tree and maps each JavaScript type to its TypeScript equivalent: string stays string, number stays number, boolean stays boolean, null becomes null (or is merged into a union), arrays become T[] where T is inferred from the elements, and objects become nested interfaces.
{
"id": 42,
"email": "ada@example.com",
"isActive": true,
"roles": ["admin", "editor"],
"profile": {
"displayName": "Ada",
"avatarUrl": null
}
}interface Root {
id: number;
email: string;
isActive: boolean;
roles: string[];
profile: {
displayName: string;
avatarUrl: string | null;
};
}Where inference has to guess
Optional vs. always-present fields: a single JSON sample can't tell a generator whether avatarUrl is sometimes missing entirely (which should be avatarUrl?: string | null) or always present but sometimes null. Feed the generator a few representative samples, or manually mark a field optional after generation if you know the API omits it under some conditions.
Numbers that are really IDs vs. quantities: JSON has one number type, but TypeScript can't distinguish an integer ID from a floating-point price without extra context. Some generators offer branded types or literal unions for this; most just emit number and leave the distinction to you.
Empty arrays: an array with no examples inside it ("tags": []) gives the generator nothing to infer the element type from — it typically falls back to unknown[] or any[]. Point the generator at a sample where the array is populated, or annotate it manually afterward.
String literal unions: a status field that's always one of "pending" | "active" | "closed" will be inferred as plain string unless the generator sees every possible value in the sample — and even then, most generators default to the broader string type unless you opt into literal-union inference explicitly.
Dates: JSON has no date type — timestamps are always strings (ISO 8601) or numbers (Unix epoch). A generator will type a date field as string or number, not Date; you still need to parse it yourself after fetching.
Generator options worth knowing
Generators expose a handful of toggles that materially change the output, and knowing what to switch on is half the value. Optional fields: most tools can emit `avatarUrl?: string` for keys absent from some samples. readonly: emitting `readonly id: number` documents which fields the client should treat as immutable, for free, at every usage site. Literal unions: instead of `status: string`, infer `status: "pending" | "active" | "closed"` from the observed values — narrower types that turn API typos into compile errors. JSDoc: descriptions attached during generation flow through to editor hovers, which is where field semantics actually get read.
One structural choice deserves attention: whether an object with heterogeneous keys — a map of user IDs to scores — becomes an index signature `{ [key: string]: number }` or a fully enumerated interface with dozens of literal keys. Generators guess based on how uniform the values are, and they guess conservatively. Check the output for any object your API genuinely treats as a map, and fix it by hand once rather than shipping a 400-key interface.
The same idea in other languages
The inference logic — walk the tree, map types, name nested objects — is the same regardless of target language; only the output syntax changes. A Go struct gets the same field mapping with struct tags for JSON key names; a Python dataclass gets type hints and an optional Optional[...] wrapper; a Java or C# class gets typed fields and getters/setters or records. If your team works across multiple languages against the same API, generating types for each from the same JSON sample keeps them honest with each other.
type Root struct {
ID int `json:"id"`
Email string `json:"email"`
IsActive bool `json:"isActive"`
Roles []string `json:"roles"`
Profile struct {
DisplayName string `json:"displayName"`
AvatarURL *string `json:"avatarUrl"`
} `json:"profile"`
}JSON Schema as the middle layer
A JSON sample can only show what it saw; a JSON Schema can state the contract — which fields are required, what the enum values are, what format a string has. When the API publishes a schema (or you can write one), generating types from the schema instead of a sample is strictly better: required drives optionality, enum drives literal unions, and descriptions become JSDoc, all without hoping your sample happened to include every variant. json-schema-to-typescript does exactly this in the JavaScript ecosystem.
The reverse also pays off: point a schema generator at several real payloads to draft a schema, tighten it by hand, then generate the TypeScript from it. Keeping the schema as the middle layer lets you derive a TypeScript client, a Go worker, and Python scripts from one source of truth instead of three hand-maintained copies that drift apart quietly.
Keeping generated types in sync
Generated types rot the moment the API changes and nothing regenerates them. The fix is to make generation a repeatable step rather than a one-time paste: keep a real sample response checked into the repo (or fetched from a staging endpoint by a script), regenerate the interface in CI, and fail the build when the output differs from what's committed. Drift then shows up as a reviewable diff instead of a runtime undefined.
Two conventions keep this pleasant. First, never hand-edit the generated file — amend it with a separate declaration that extends or narrows the generated interface (`interface User extends UserGenerated { status: UserStatus }`). Second, put a "generated — do not edit" banner at the top of the file; six months later, nobody will remember which file was which. When the upstream API is versioned, bump the checked-in sample and the generated types in the same commit that migrates the client code.
Before you trust generated types
Generated types are a strong starting point, not a final contract. Before committing them: confirm optional fields against real API documentation (not just one sample), replace loose string fields that are actually enums with literal unions, double-check that empty-array fields got a real element type from a better sample, and re-generate whenever the upstream API adds or renames a field rather than hand-patching the interface.
FAQ
- Can a JSON-to-TypeScript generator handle a field that's sometimes a string and sometimes a number?
- Yes, if it sees both variants across the samples it's given — it should infer string | number. With only one sample, it can only type what it saw. This is the main reason to generate from multiple representative payloads rather than a single happy-path example.
- Should generated interfaces use `interface` or `type`?
- Functionally near-identical for this use case. `interface` is more common for object shapes and supports declaration merging; `type` is more flexible for unions and intersections. Either works for API response shapes — pick whichever matches your codebase's existing convention.
- How do I keep generated types in sync as the API evolves?
- Treat generation as a repeatable step, not a one-time copy-paste: keep a real sample response checked in (or fetched from a staging endpoint) and regenerate the interface whenever the sample changes, rather than hand-editing the generated file. Some teams wire this into CI to catch drift automatically.
- Does this work for deeply nested or recursive JSON, like a tree structure?
- Deep nesting generates nested interfaces fine. Truly recursive structures (a comment that can contain replies of the same shape) need a self-referential type, which most generators won't infer automatically from a single sample — you'll typically need to hand-write the recursive interface once and reuse it.
- Should I commit generated type files, or generate them at build time?
- Commit them. Build-time generation couples your build to a staging API's availability and makes code review blind to type changes — a teammate updating a handler won't see that the response type shifted underneath them. Committing the generated file, plus a CI check that regenerates and diffs it, gives you reviewable, reproducible types with drift caught automatically.
Try these tools
JSON to TypeScript →
Generate TypeScript interfaces from JSON with optional readonly modifiers and JSDoc comments.
JSON to Go →
Generate Go structs with json tags from a JSON sample, ready for encoding/json.
JSON to Python →
Generate Python TypedDict classes from a JSON sample for mypy, Pyright, and editor autocompletion.
Related articles
What is JSON? A Complete Beginner's Guide →
JSON (JavaScript Object Notation) is the lightweight data-interchange format behind nearly every API, config file, and NoSQL database. Learn the syntax, the six data types, how parsing behaves in code, and the mistakes everyone makes hand-writing it.
The History of JSON: From 2000 to Industry Standard →
Trace JSON from Douglas Crockford's idea in 2001, through Yahoo's adoption, to becoming the ECMA-404 standard that powers 90% of modern APIs.
The Complete JSON Schema Guide (Draft 7) →
JSON Schema is the standard for describing and validating JSON structure. Learn the core keywords, build a real API schema, compose and reuse schemas with $ref, and run validation in code with Ajv.