The fastest honest way to type an API: paste a real payload, get interfaces back, and tighten by hand. This guide covers the inference rules that matter — unions for mixed arrays, null handling, merged array-of-object shapes — plus the three places a sample can't tell the truth, so your generated types don't turn into runtime surprises.
Hand-writing an interface from API docs fails quietly: docs lag, fields get renamed, and optional things turn out to be conditional. Typing from a curl response flips the source of truth — the types describe data that provably existed. It's the same instinct as generating database types from a live schema instead of an ER diagram someone drew in 2022.
The catch is that one sample is evidence, not a contract. Generation gets you 90% of the way in seconds; the last 10% (optionality, enums, dates) still needs a human. Used that way, sample-to-type is a productivity tool. Used as a contract, it's a bug generator.
| JSON | TypeScript | Why |
|---|---|---|
"x" / 1 / true | string / number / boolean | Direct mapping |
null | null | JSON has no undefined; hiding nulls invites crashes |
["a","b"] | string[] | One element type wins |
[1,"two"] | (number | string)[] | Union of distinct types, sorted for stable output |
[{"a":1},{"b":"x"}] | one merged Item interface | Fields union per key; that's the shape you program against |
[] | unknown[] | An empty array proves nothing about elements |
| nested object | named interface per path | address under Root becomes RootAddress |
The merged-array rule deserves a note, because it's where naive converters differ. Given two orders where one has a discount field and the other doesn't, the honest generated type unions what exists — discount?: ... territory — rather than emitting two near-identical interfaces you'd have to handle separately at every call site. Merge-first keeps the type ergonomics of one array, one item type.
{
"id": 1,
"name": "Ada Lovelace",
"active": true,
"score": null,
"tags": ["math", "pioneer"],
"address": { "street": "12 Analytical Way", "city": "London", "zip": null },
"orders": [
{ "id": 101, "total": 59.99, "items": ["book", "slide rule"] }
]
}
With strict nulls kept, generation produces three interfaces:
interface Root {
id: number;
name: string;
active: boolean;
score: null;
tags: string[];
address: RootAddress;
orders: RootOrdersItem[];
}
interface RootAddress {
street: string;
city: string;
zip: null;
}
interface RootOrdersItem {
id: number;
total: number;
items: string[];
}
Switch strict nulls off and the two null fields become any — which is exactly what your types behave like in a codebase that doesn't run strictNullChecks. Prefer keeping nulls and letting the compiler force the ?? fallback; deleting them from the type doesn't delete them from the payload.
Paste JSON, pick a root name, copy the interfaces. Runs 100% client-side — your payload never leaves the browser.
JSON to TypeScript Converter →?: by hand from docs or multiple samples."status": "active" looks like string but is really "active" | "paused" | "closed". One sample shows one value. Tighten these manually — they're also the fields most likely to break your UI when a new value ships.string and parse at the boundary, or brand them (type UserId = string & {__brand: 'UserId'}) so a UserId can't be passed where a PostId goes.Three patterns, in increasing order of rigor. First, one-off: paste a payload, copy the types, adjust. Second, fixture-driven: keep a __fixtures__ directory of real responses, regenerate on demand, and diff the output in code review — type churn becomes visible, which is the whole point. Third, schema-driven: if the backend publishes OpenAPI or JSON Schema, generate from that instead, because a schema states optionality and enums where a sample can't. The sample-first tools still win for third-party APIs that document with a shrug.
Whatever the pattern, keep generated files clearly marked and mechanically produced. The moment someone hand-edits a generated interface, regeneration becomes a conflict, and the file rots. For the surrounding workflow, the JSON formatter cleans up minified payloads before conversion, and the JSON schema validator is the right next stop when you're ready to move from samples to a real contract.
It's a privacy boundary, and occasionally a hard requirement. API payloads routinely contain real user data, tokens, or internal identifiers, and pasting those into a random web converter means shipping them to whoever runs it. A converter that parses and generates in the browser with no network call keeps the payload on the machine it came from. Our converter works that way, and its engine is Apache-2.0, so you can also lift the generation logic into a script for CI or codegen steps.
Not from a raw string. A JSON string is just string to the compiler. You either generate types from a sample (paste it into a converter), from a JSON Schema, or cast with a type assertion — which skips checking entirely. Generation from a real payload is the middle ground: the types describe actual data instead of a guess.
A sample shows what one response looked like; a schema states what every valid response may look like — required versus optional fields, enums, ranges, formats. Sample generation gives you shapes fast but cannot prove a field is optional or constrain a value. If you have a schema, validate against it; if you only have examples, generate from the fattest example you can find and tighten by hand.
You can't infer them from one sample — absence in a sample doesn't prove a field can be absent. The practical workflow: generate from a sample, then mark fields optional (name?: string) where the API docs or several samples say they come and go. Feeding several payloads into the converter helps: fields that appear in one array element and not another get unioned, which is the hint.
For object shapes they compile the same. Interfaces allow declaration merging, so you can extend a generated Root elsewhere without editing the generated file — handy when the file is regenerated. Type aliases can express unions, tuples and primitives. A common convention: interfaces for object payloads, aliases for the top-level when the root is an array or primitive.