Developer & encoding

How to Generate a TypeScript Interface from JSON

Turn a JSON response into TypeScript interfaces. How optional and nullable differ, why one sample is never enough, and the fields a generator gets wrong.

6 min readUpdated Sep 16, 2026

You have a JSON response from an API and you want a TypeScript type for it. Writing one by hand is slow and, worse, quietly inaccurate: you type what the response happened to contain the day you looked, so fields that are sometimes absent come out required and fields that are sometimes null come out non-nullable - and the compiler then tells you your code is safe when it is not. Generating the type from real data is faster and more honest, but only if the generator reads the data properly. This guide covers what a good conversion does and how to read the result.

The three things a generated type has to get right

  • Which fields are optional - present in some records, absent in others.
  • Which fields are nullable - always present, but sometimes holding null.
  • What a nested object or array really looks like across every record, not just the first.

Everything else - indentation, interface or type, exported or not - is preference. Those three are correctness, and they are what a five-line converter gets wrong.

A worked example

Take a response with three users in it, where the second has a null email and the third has no email key at all and carries an extra admin flag:

{ "page": 1, "total": 3, "users": [ { "id": 7, "name": "Ada", "email": "ada@example.com", "team": "core" }, { "id": 8, "name": "Lin", "email": null, "team": "core" }, { "id": 9, "name": "Ravi", "team": "growth", "admin": true } ] }

Paste that into the JSON to TypeScript generator, set the root name to ApiResponse, and you get two interfaces:

export interface ApiResponse { page: number; total: number; users: User[]; }

export interface User { id: number; name: string; email?: string | null; team: string; admin?: boolean; }

Three details are worth pulling out. The users array became User[] rather than an inline type, singularised from the key. The admin flag is optional, because only one of the three records had it. And email is both optional and nullable - missing from one record, null in another, which are two separate facts about one field.

Optional and nullable are not the same thing

This is the distinction most converters collapse, and the one that matters. A question mark means the key may not exist at all, so reading it gives undefined. A union with null means the key is always there and may hold null. They need different code - user.email?.trim() for the first, user.email !== null for the second - and guarding the wrong one leaves the crash you were trying to prevent.

A generator that emits any for anything uncertain, or that marks nullable fields optional because it silences the compiler, throws the distinction away. So does one that ignores null and types email as string - the worst of the three, because the type now asserts something the data contradicts.

Why one record is never enough

Optionality is not visible in a single object. Feed a generator one user and every field comes out required - nothing in one sample could say otherwise. The type compiles, the code passes review, and the first record in production without an email fails at runtime instead of at build time, which is exactly the failure TypeScript was meant to move earlier.

So paste an array of several records. The generator reads every element, merges the shapes and marks a field optional whenever some records lack it. This is also why reading only the first element - which is what a naive converter does, because it is far easier - is worse than it sounds: a field that first appears in the second record vanishes from the type altogether, and the property you needed becomes a compile error on data that is perfectly valid. The tool reports how many records the best-sampled shape was built from, and says so plainly when it only had one.

Include the awkward rows on purpose - the record with the empty list, the one with the null, the one still carrying a legacy field. A type generated from the happy path only describes the happy path.

Keys that TypeScript cannot use as they are

A JSON key can be any string, while a TypeScript property name has to be an identifier - so "first name", "content-type", "2fa" and the empty string are quoted in the interface and read back as obj["first name"]. A generator that renames them to look prettier produces a type that no longer matches the data, so quoting is the right answer even though it is the uglier one.

Interface names are a separate problem, because they are derived from keys rather than being keys. They are PascalCased, singularised when they name an array, and kept away from the standard library's own type names - declaring an interface called Object merges with the built-in declaration rather than shadowing it, which breaks the file in a way the error message does not explain. Identical shapes also share one interface: an author and an editor that both hold a name and an email should not become two identical types.

The numbers that do not survive

TypeScript has one numeric type for JSON numbers, and it is a 64-bit float. Integers above 2^53 - roughly 9.007 quadrillion, or sixteen digits - cannot be held exactly, so a Discord or Twitter style snowflake ID typed as number comes back with its last digits changed. The generator flags those rather than typing them silently, and the fix is to declare the field as string or bigint.

Check the other end too: JSON.parse does the same damage before any type is involved, so an annotation will not save an ID already rounded on the way in. The JSON Formatter shows you the raw text if you need to look.

When to trust a string literal union

A status field holding "active" or "archived" is more useful typed as "active" | "archived" than as string, because the compiler then catches a typo in a comparison. But inferring that from a sample is a guess, and a wrong guess rejects valid data. So it is an option rather than the default, and a union is only inferred on evidence of a closed set: at least three records carried the field, at most twelve distinct values, and at least one value repeating. Three rows with three different values are three strings, and stay string.

Even then it is a starting point. A sample cannot show you the status your API returns once a month, so the honest version of the type is usually the generated union plus whatever the documentation lists.

A generated type is a description, not a guarantee

Worth ending on, because it is the mistake that outlives the others. An interface is erased at compile time, so const data: ApiResponse = await res.json() checks nothing at runtime - it asserts, and if the API changes next Tuesday your code carries on believing the old shape until something breaks somewhere unhelpful. Generated types are still excellent for editor completion, for catching typos and for documenting what an endpoint returns, and the JSON to TypeScript generator gives you one in seconds from a response you already have. If you need the data actually checked, validate it at the boundary with a schema library and derive the type from that schema instead. Generate the interface to learn the shape; keep the runtime check for the shape you cannot control.

Frequently asked questions

How do I create a TypeScript interface from a JSON response?
Copy a real response, paste it into the generator, and give the root type a name. The key is what you paste: an array of several records rather than a single object, because a single object cannot show which fields are optional. Every element is read and the shapes merged, so a field missing from some records comes out with a question mark, a field holding null comes out as a union with null, and nested objects become their own named interfaces with the array keys singularised - a users key produces User[]. You can then choose interface or type declarations, 2 or 4 space indentation, whether the declarations are exported, and whether every property is marked readonly.
What is the difference between name?: string and name: string | null in a generated type?
They describe two different things and need different code. The question mark means the key may be absent from the object, so reading it gives undefined and you need optional chaining or a check against undefined. The union with null means the key is always present but may hold null, so you need a check against null. A field that is sometimes missing and sometimes null is written name?: string | null. Most generators collapse the two, usually by marking everything optional or by typing anything uncertain as any, and that is where a type stops protecting you: code guarding for undefined still crashes on null.
Is a generated TypeScript interface enough to validate API data?
No, and this is the most common misunderstanding about them. TypeScript types are erased when your code is compiled, so an interface on an API response is an assertion rather than a check - nothing at runtime confirms the data matches, and if the endpoint changes your code keeps believing the old shape until something breaks further downstream. A generated interface is genuinely useful for editor completion, for catching typos at compile time and for documenting what an endpoint returns. If you need the data actually verified, validate it at the boundary with a runtime schema and derive the TypeScript type from that schema instead of from a sample.