Key takeaways
- Generated types describe samples, not the entire API contract.
- Multiple representative records improve array and optional-property inference.
- Null, missing, and undefined must be modeled deliberately.
- Runtime validation is still required at untrusted boundaries.
Turning JSON into TypeScript is useful because real data provides a fast starting point for object shapes. It is also easy to overstate what one sample proves. If a field contains 42, the generator can observe a number; it cannot know whether the production API may send null, omit the field, return a numeric string, or use a larger range.
Use JSON to TypeScript to produce an initial interface from representative data, then review it as a proposed contract. The strongest workflow combines several samples, API documentation or a schema, runtime validation, and strict compiler settings.
Observation is not specification
JSON has objects, arrays, strings, numbers, booleans, and null. It has no interface names, optional-property marker, date type, integer distinction, tuple declaration, branded identifier, map, or union discriminator. A converter must infer or invent those TypeScript concepts.
One object cannot reveal optionality because every visible key is present. Multiple objects may show that a key is absent in some records, but absence could mean optional, permission-dependent, version-dependent, or a bad sample. A string that looks like an ISO timestamp remains a string until your application parses and validates it.
Generated names are guesses too. A key named users may sensibly produce User and User[], but irregular plurals, acronyms, domain terms, and nested anonymous objects need human editing. Good generation creates compilable, readable scaffolding and clearly distinguishes certainty from heuristic choices.
Start with representative data
Collect successful responses, empty collections, nullable cases, optional fields, alternative statuses, and error payloads. Remove credentials and personal data before pasting anything into an online utility. Preserve shape while replacing sensitive values with safe examples.
[
{ "id": "u_1", "name": "Amina", "role": "admin", "lastLogin": "2026-08-20T09:30:00Z" },
{ "id": "u_2", "name": "Noah", "role": "member", "lastLogin": null },
{ "id": "u_3", "name": "Mei", "role": "member" }
]
The sample supports id and name as strings. It suggests role might be a literal union, but only documentation can confirm the complete set. lastLogin appears as string, null, or missing. A cautious type is lastLogin?: string | null, followed by runtime validation and conversion if the application needs a Date.
A reliable conversion workflow
- Define the boundary. Name the API endpoint, event, storage record, or configuration file the type represents. Avoid one global “Data” interface.
- Gather varied samples. Include different statuses, empty and populated arrays, nullable records, and known error shapes.
- Validate JSON syntax. Comments, trailing commas, NaN, Infinity, and undefined are JavaScript values but not valid JSON.
- Generate a first draft. Choose interfaces or type aliases according to project convention. Use stable names rather than accepting every generated anonymous label.
- Review arrays. Empty arrays provide no element evidence. Mixed arrays may require a union, tuple, normalization, or a broader domain model.
- Review absence. Distinguish a missing property from a property whose value is null. Under exactOptionalPropertyTypes, optional does not automatically mean explicitly undefined.
- Add domain meaning. Refine status strings into documented unions, identify opaque IDs, and keep dates as strings until parsing occurs.
- Add runtime validation. Parse unknown input with a schema or explicit guards, then expose the validated TypeScript type to application code.
- Test contract drift. Fixtures and integration tests should fail clearly when upstream data changes.
Interfaces, aliases, and generated names
Interfaces work well for extensible object contracts and produce familiar error messages. Type aliases are convenient for unions, intersections, primitives, and mapped types. Either can describe most object shapes; consistency and clear domain naming matter more than a blanket rule.
Avoid creating a distinct interface for every nested object when several represent the same concept. Conversely, do not merge objects merely because current properties happen to match. A billing address and shipping address may evolve differently even when today’s JSON is identical.
Name types at boundaries: UserResponse, SearchResult, InvoiceLine, or WebhookEvent. Root, Item, and Data become ambiguous as a codebase grows. A generator should let you select or edit the root name before the output becomes shared code.
Null, missing, and undefined
JSON can encode null and can omit an object property. JSON cannot encode undefined. TypeScript can model all three, and strictNullChecks makes the differences visible.
Use property?: T when the key may be absent. Use property: T | null when the key is present but can explicitly contain null. Use property?: T | null when both occur and the external contract permits both. Do not automatically replace everything with optional properties to eliminate errors; that spreads defensive checks throughout the application without documenting reality.
Normalize at a boundary when the application wants a simpler internal model. For example, convert missing and null lastLogin values into one internal null, or reject records that violate the documented API. The normalization code is executable documentation and a natural place for telemetry about unexpected input.
Arrays and unions need more evidence
An empty array reveals no element type. A generator might choose unknown[], never[], or any[]; none is a discovered domain contract. Supply a populated example or edit the result from documentation.
Mixed object arrays may represent a discriminated union. If every variant has a stable kind field, model each variant separately and use a union. If records simply have inconsistent optional fields, a single interface may be more accurate. Look for a semantic discriminator rather than generating a large union from accidental sample differences.
Literal unions should also be conservative. Seeing admin and member does not prove those are the only roles. Prefer string until the API documents a closed set, or include an unknown fallback strategy so a new server value does not crash the client.
Common mistakes
- Generating from one happy-path response and declaring every visible field required.
- Converting date-looking strings directly to Date even though JSON parsing returns strings.
- Using any for empty arrays or unknown nested values and losing boundary safety.
- Treating null and absence as interchangeable without checking the API contract.
- Inferring a closed literal union from an incomplete sample.
- Pasting production secrets or customer records into a browser tool.
- Trusting a TypeScript assertion as runtime validation.
- Sharing generated names such as RootObject across unrelated endpoints.
Review checklist
- The type name identifies the boundary and domain concept.
- Samples include alternative states, nulls, missing keys, and empty arrays.
- Optional and nullable meanings match documentation.
- Arrays have justified element types.
- Literal unions are known to be closed or have a fallback.
- External timestamps remain validated strings until converted.
- Input enters application code as unknown and is checked at runtime.
- Contract fixtures avoid personal and secret data.
Frequently asked questions
Can TypeScript validate an API response by itself?
No. Type information is erased from emitted JavaScript. The compiler checks how your code uses declared types; it does not inspect a production network response. Use runtime validation before treating external data as a trusted type.
Should generated properties be readonly?
Readonly can express that application code should not mutate a response object, especially when data is cached. It does not freeze runtime objects. Choose it as part of the internal ownership model, not because JSON itself is immutable.
How should I type numbers that must be integers?
JSON has one number category and TypeScript number does not enforce integer ranges. Validate integer, bounds, and special domain rules at runtime. A branded type may help after validation, but the brand alone does not perform the check.
What should an empty array become?
Use documentation or another sample to supply the element type. If the element truly is unknown, unknown[] accurately forces later checking. Avoid any[] because it silently disables useful checks.
Further reading
Generated types are most valuable as a structured question: “Does this draft match the real contract?” Answer that question with representative evidence and runtime validation before the type becomes an assumption across the codebase.
Try the related tool
Apply the guide to a focused example, then verify the result in your own project.
Open the related tool →