Skip to main content
TypeScript

JSON to TypeScript interfaces: the generated type is a guess, not a contract

A type generated from one JSON sample lies about nulls, empty arrays, and unions. Where the inference goes wrong, and how to harden it before you ship.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 3 min read

A generated TypeScript interface is not a schema. It's a snapshot of one response, dressed up as a guarantee. The compiler will happily let you write order.customer.email.toLowerCase() against a type that was inferred from a single sample where customer happened to be present — and then throw Cannot read properties of null in production when the next response omits it. TypeScript erases every one of these types at runtime; nothing checks that the JSON actually matches.

That doesn't make generators useless. Pasting a payload and getting a first draft of the shape saves real mechanical typing. The trap is trusting the draft.

An interface describes what you hope arrives. Runtime validation describes what did arrive. You need both, and only one of them survives to runtime.

Where the inference goes wrong

Take a real-looking payload and watch a generator guess:

{ "id": "ord_9", "coupon": null, "items": [], "status": "paid" }

Most generators emit something like this:

interface Order {
  id: string;
  coupon: null;        // <- inferred from one null
  items: any[];        // <- empty array, no element type
  status: string;      // <- "paid" is really one of a fixed set
}

Line by line, here's the gap between the guess and reality:

FieldGenerator infersActual truthFix
couponnullstring | null — a code when presentWiden to the real type plus null
itemsany[]Array<{ sku: string; qty: number }>Sample a non-empty response
statusstring"paid" | "pending" | "refunded"Make it a union
idstringstring (correct)Leave it

The corrected version is the one worth committing:

interface Order {
  id: string;
  coupon: string | null;
  items: Array<{ sku: string; qty: number }>;
  status: "paid" | "pending" | "refunded";
}

Takeaway: the generator got exactly one of four fields right on its own. The other three each required knowing something the sample couldn't show you — that a null is sometimes a string, that an empty array has an element type, that a string is really an enum.

Make the boundary do the checking

The durable fix isn't a better interface — it's a validator at the network boundary. Parse the response with something like Zod or Valibot, and let the validated output infer your type. Now the shape is enforced at runtime and the TypeScript type is derived from that enforcement, so they can't drift apart. Map the external payload into your own internal model there too, so a vendor renaming a field is a one-line change instead of a codebase-wide find-and-replace.

Use the JSON to TypeScript converter to skip the mechanical first draft. Then spend the time you saved on the part that actually protects you at runtime: a validator on the wire and a mapping into a model you control.

Cover photo by Seraphfim Gallery on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

TypeScriptJSONAPIs

Related articles

All articles