JSON to TypeScript

You have one sample response, no OpenAPI spec, and a function to write against it this afternoon. Pasting the sample here gets you a set of interfaces to start from in a few seconds — which is worth more than the twenty minutes of typing it replaces, and considerably less than a schema you can actually rely on. The difference is the whole subject of the notes below.

JSON to TypeScript — Generate Interfaces from a Sample ResponseBuildFigure

How the types are derived

Each JSON value maps to the obvious TypeScript type: string, number, boolean, null. Objects become their own named declaration, with the name taken from the key that held them and converted to PascalCase, so stock produces Stock. Arrays produce an element type followed by [], and the element type is named from the singular of the key, so lines produces Line[]. A name collision gets a numeric suffix. Keys that are not valid TypeScript identifiers are quoted, which is why content-type comes out as "content-type": string and has to be read with bracket access.

Merging array elements is where the useful inference happens

Every object in an array is folded into one type. A key present in all of them is required; a key present in some is marked optional with ?. A key whose value type varies between elements becomes a union, so an id that is 1 in one element and "2" in another produces number | string. A mixed array such as [1, "a", null] becomes (number | string | null)[].

Both of those outputs are more useful as signals than as types. An unexpected optional means the field is genuinely absent sometimes, which your code has to handle. An unexpected union usually means the API is inconsistent about serialising something, and the right response is to go and ask rather than to accept number | string into your codebase.

interface or type

For describing an object shape the two are close to interchangeable. interface supports declaration merging — declare the same name twice and the members combine — and reads slightly better in implements clauses. type is the only one that can express a union, a tuple, a mapped or a conditional type. In practice most codebases pick one for object shapes and use type where they have no choice. This page follows your selection, except at the root: if the top-level value is an array or a primitive, it has to emit a type alias regardless, because an interface cannot alias one.

The three places single-sample inference is wrong

An empty array carries no information about its elements, so [] becomes unknown[]. That is honest, and it is also a placeholder you have to fill in yourself.

A field that was null in the sample is typed null and nothing else. The real type is almost certainly string | null or similar; the sample simply did not contain a populated one. Every | null in the output deserves a moment's thought about what the other half should be.

A field that was present in every sampled element is marked required, and that is the dangerous one, because it is silent. Optional fields announce themselves; a field that is optional in reality but present in your one sample produces a type that compiles and then breaks at runtime on the response that omits it. Sample size is the only defence, and one is a small sample.

What it deliberately does not attempt

Date strings come out as string, because that is what they are on the wire; converting to Date is a runtime parsing decision, not a type-level one. A string field holding "active" comes out as string rather than a literal union, because nothing in the sample distinguishes an enum from free text — narrow it by hand once you know the values. Two structurally identical objects under different keys produce two identical declarations; the tool names types by key, not by shape, so deduplicate manually if it bothers you. And an integer past 253 is already damaged by JSON.parse before typing begins, so no annotation will save it.

Use the output as a first draft. When a real schema exists — OpenAPI, protobuf, a shared types package, the server's own definitions — generate from that instead and let it stay in sync. This is for the case where none of those exist yet. The sample is parsed in your browser and goes nowhere, so a production response with real data in it is fine to paste.

Questions people ask

My key has a hyphen in it and the output looks odd.

Keys that are not valid TypeScript identifiers are emitted as quoted property names, so a header map comes out with "content-type": string. That is correct and it compiles. You just cannot use dot access on it — write obj["content-type"], or rename the field when you map the response into your own model.

Can it produce string literal unions or enums?

No, and it should not guess. A sample containing "active" gives no way to tell whether the field takes one of three known statuses or any string at all. Emitting a literal union would produce a type that rejects perfectly valid future responses. Generate string, then narrow by hand once you have the list of permitted values from documentation or from the people who wrote the endpoint.

Identical objects in two places generated two identical types.

Types are named from the key that contained them, and no structural deduplication is done. If address and shippingAddress hold the same shape you get Address and ShippingAddress with the same members. Keep one, delete the other, and update the reference — it is a two-line edit and it is more predictable than a tool guessing that two same-shaped objects mean the same thing.

How many samples should I feed it?

More than one, and pick them for variety rather than volume. The inference gets better exactly where samples disagree: a key missing from one element becomes optional, a type that varies becomes a union. Paste an array containing several real responses, including the awkward ones — an empty result, a record with the optional fields unset, an error-shaped payload — and the generated types will reflect what your code actually has to handle.

Related