Skip to content
/>codemaxxed.com
October 20266 min read

JSON to TypeScript: types that survive real APIs

Pasting a JSON response into a type generator is the fastest way to stop writing interfaces by hand. It is also how teams end up with a type that lies. A single sample can tell you a lot about shape and almost nothing about what the API will do next Tuesday.


What the generator can actually see

Type inference walks the values in your sample. Strings become string, integers and decimals both become number, booleans become boolean, and objects become interfaces. Arrays are the interesting case: every element is inspected and the shapes are merged.

{
  "id": 8241,
  "name": "Ada Lovelace",
  "roles": ["admin", "editor"],
  "orders": [
    { "id": 1, "total": 42.5 },
    { "id": 2, "total": 18, "refunded": true }
  ]
}

Optional means missing, not null

In the sample above, refunded appears in one order and not the other, so the inferred type marks it optional: refunded?: boolean. That is accurate for a key that may be absent. It is not the same as a key that is always present but only ever null: a single null tells the generator nothing about the real value, so it widens to unknown and leaves the decision to you. Confusing the two is how you get a runtime crash that TypeScript swore was impossible.

Arrays merge instead of guessing

If an array holds a string and a number, the item type becomes (string | number)[] rather than picking the first element. If it holds two objects with different keys, both keys appear and both are optional. This is the conservative behaviour, and it is the one you want: a type that under-promises is safe, a type that over-promises is a bug waiting for a slow Tuesday.

An empty array is unknowable. The generator emits unknown[] rather than inventing a shape. Fill it in by hand once you know what belongs there.

Three things inference cannot know

  • Whether a field is required by the API contract. Presence in one response proves nothing. If the docs say required, make it required.
  • Whether a string is a date, an enum, or a UUID. All three arrive as string; TypeScript cannot recover intent from a value.
  • Whether an integer is safe. JavaScript numbers lose precision above 2^53, so large IDs should be typed and parsed as strings.

Harden it before shipping

Treat generated types as a first draft. Rename Root to the thing the endpoint returns, add unions for fields with a known set of values, and validate at the boundary. Types disappear at runtime, so an API that changes shape will still send undefined into your UI unless something checks.

That is what a schema library is for. Generate types for the editor, and a Zod or Pydantic schema for the request boundary. The two together catch both the mistakes you can see and the ones you can't.

Turn a real response into interfaces, then generate a Zod schema for the runtime check.

Open the TypeScript generator