JSON to TypeScript Types: Inference and Validation
Turn API responses into TypeScript types, handle optional fields, nulls and unions, and learn why types alone don't validate data at runtime.
Editorial disclosure: Pyalm publishes and maintains the products and free tools discussed on this site. Regulatory statements are linked to primary sources where applicable. No independent professional review is claimed unless a reviewer is explicitly named.
Typing an API response by hand is slow and error-prone. Generating types from a sample JSON payload is a fast start — as long as you understand what the generated types do and don't guarantee.
From sample to type
Given this response:
{
"id": 1042,
"customer": { "name": "Al Noor Trading", "email": null },
"items": [
{ "sku": "A-100", "qty": 2, "price": 49.5 },
{ "sku": "B-200", "qty": 1, "price": 120, "discount": 10 }
],
"paid": false
}
A generator produces:
export interface Root {
id: number;
customer: Customer;
items: Item[];
paid: boolean;
}
export interface Customer {
name: string;
email: null;
}
export interface Item {
sku: string;
qty: number;
price: number;
discount?: number;
}
Notice three decisions the generator made: nested objects became named interfaces, the array elements were merged into one Item type, and discount became optional because only one item had it.
What a sample can't tell you
A type inferred from one payload only describes that payload. Review these before trusting it:
- Nulls.
email: nullin the sample is probablystring | nullin reality. - Unions and enums. A
status: "paid"value might be one of several strings. Replacestringwith a literal union. - Empty arrays infer as
never[]. Add a representative item. - Optional vs nullable. Missing keys and null values mean different things; model them differently (
field?: Tvsfield: T | null). - Dates arrive as strings. Keep them
stringin the wire type and convert at the boundary. - Large integers. IDs beyond 2^53 lose precision as JavaScript numbers; use strings.
- Money. Decide whether amounts are integers in minor units or decimal strings — see floating-point money bugs.
interface or type?
Both describe object shapes. Interfaces can be extended and merged and give clearer errors; type aliases are needed for unions and mapped types. Consistency matters more than the choice.
Types vanish at runtime
TypeScript types are erased when the code compiles. If the server returns something unexpected, response.json() as Root still "works" until it crashes somewhere later. For data from outside your code — APIs, forms, files, local storage — validate at runtime:
import { z } from "zod";
const Item = z.object({
sku: z.string(),
qty: z.number().int(),
price: z.number(),
discount: z.number().optional(),
});
const Root = z.object({
id: z.number(),
customer: z.object({ name: z.string(), email: z.string().nullable() }),
items: z.array(Item),
paid: z.boolean(),
});
type Root = z.infer<typeof Root>;
const data = Root.parse(await res.json()); // throws on bad data
Schema libraries let you derive the static type from the runtime schema, so there's one source of truth.
A workflow that scales
- Generate a first draft from a real response.
- Tighten it using the API documentation or schema (OpenAPI makes this automatic).
- Add runtime validation at the boundary.
- Keep types in one place, import everywhere.
- Re-check when the API changes — see how to test a REST API.
Generate types now
Paste a JSON sample into the free JSON to TypeScript Converter: nested objects become named interfaces, keys missing from some array items become optional, mixed values become unions, and you can choose interface or type, export and readonly. Clean the sample first with the JSON Formatter, or turn a spreadsheet export into JSON with the CSV ⇄ JSON Converter. Everything stays in your browser.
Need an API built well?
Typed, validated, documented APIs are part of every integration and mobile or web app we build at Pyalm. See custom software development or get in touch.