On this page
An API response can look perfectly typed in your editor and still crash a production screen. Generating an interface removes repetitive typing, but it does not prove that tomorrow's response will match today's sample.
This workflow turns representative JSON into maintainable contracts, adds runtime checks where data enters your application, and keeps transport details out of presentation components.
Start with evidence, not a single response
The sample coverage determines the usefulness of generated types. One successful response cannot reveal fields omitted for anonymous users, empty collections, or nullable values. Gather several sanitized fixtures before using the JSON-to-TypeScript Interface Converter below. Include the ordinary response, a sparse response, and at least one populated nested collection.
Treat generated interfaces as a contract draft, not an authoritative specification. An empty array reveals no element type. A field containing only null provides no evidence about its non-null shape. A numeric identifier in one response does not guarantee every endpoint uses numbers.
Write down the uncertain decisions alongside the fixtures. Ask whether prices use minor currency units, whether timestamps include timezone offsets, and whether missing fields mean unavailable or unauthorized. Resolving these questions prevents more defects than choosing between interface and type aliases.
For malformed fixtures, use the JSON Formatter & Syntax Linter before conversion. Formatting improves inspection; it does not validate business meaning or make sensitive production data safe to share.
Review the generated contract deliberately
A useful transport model mirrors the wire format. Keep names such as published_at unchanged initially rather than silently renaming them in an interface. Conversion belongs in an explicit adapter that the team can test.
Consider a product response with an ID, a title, nullable inventory, and a list of image records. Inspect every generated member rather than accepting the output wholesale. In particular, distinguish an optional property from a property whose value may be null.
- Use
inventory: number | nullwhen the key always exists but inventory can be unknown. - Use
inventory?: numberwhen the server may omit the key entirely. - Use a documented union for known states instead of narrowing from one observed string.
- Keep money as integer minor units when that is the actual API contract.
Do not infer a literal union from accidental sample values. Seeing only "draft" does not establish that "published" is impossible. Conversely, replacing documented states with unrestricted strings discards useful checks.
The MDN JSON reference explains the data format's limited vocabulary. JSON has no native date, bigint, or undefined value. TypeScript declarations cannot add those capabilities to the incoming payload.
Validate unknown data at the boundary
Static checking operates on your program, while runtime validation inspects actual values. A type assertion after response.json() does not inspect anything. Assign incoming data to unknown, then validate before exposing it to the rest of the application.
This example accepts extra server fields but checks every field the component needs. It also rejects negative, fractional, or unsafe inventory counts. An application with many contracts should use a maintained schema validator rather than duplicating large handwritten guards.
type ProductDTO = {
id: string;
title: string;
inventory: number | null;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function parseProduct(value: unknown): ProductDTO {
if (!isRecord(value)) throw new Error("Expected product object");
const { id, title, inventory } = value;
if (typeof id !== "string" || id.length === 0) {
throw new Error("Invalid product ID");
}
if (typeof title !== "string" || title.length > 200) {
throw new Error("Invalid product title");
}
if (inventory !== null &&
(typeof inventory !== "number" ||
!Number.isSafeInteger(inventory) || inventory < 0)) {
throw new Error("Invalid inventory");
}
return { id, title, inventory };
}
async function loadProduct(id: string): Promise<ProductDTO> {
const response = await fetch("/api/products/" + encodeURIComponent(id));
if (!response.ok) throw new Error("Product request failed");
const payload: unknown = await response.json();
return parseProduct(payload);
}Keep validation failures distinct from network failures internally, even if both show a friendly retry message. Record an endpoint name and failed field, not the complete customer payload. The MDN Fetch guide covers response handling, including why HTTP error statuses require explicit checks.
Adapt transport objects into screen models
A view model should express what a component needs rather than every detail the server happens to return. Convert timestamp strings to display values in one adapter, combine related fields deliberately, and decide how unknown inventory appears before rendering.
For example, map null inventory to a visible "Availability unknown" label rather than zero. Zero means sold out; unknown means the application lacks information. Those states need different copy and possibly different purchase behavior.
Keep conversion rules explicit. If an endpoint returns cents, divide by 100 only at the formatting boundary and preserve currency alongside the amount. If it returns timestamp strings, reject unsupported formats before constructing dates. The MDN Date reference documents parsing behavior and timezone pitfalls.
Avoid making every component handle transport failures independently. A data-access function can return validated data or a controlled error, while a route-level boundary chooses the loading, retry, or empty state. This reduces repeated checks without pretending remote data is trustworthy.
For broader UI state design, pair this approach with asynchronous response handling. A sound data model still needs a screen that explains when information is missing.
Automate regeneration without hiding changes
Make contract review part of the pull request. Store sanitized fixtures in version control, regenerate the draft when the upstream response changes, and inspect the diff. A newly nullable field should prompt a behavior decision, not simply another question mark.
Return to the interface converter when exploring a new endpoint, but establish one source of truth for production contracts. If an upstream schema already exists, prefer generating from that schema and checking its version rather than repeatedly inferring from samples.
Add negative fixtures alongside successful examples. Test an omitted ID, a null title, a fractional inventory value, an unexpected array, and an extra harmless property. The last case confirms whether your parser intentionally allows additive server changes.
- Capture and sanitize representative responses.
- Generate the initial interfaces and resolve ambiguous fields.
- Implement runtime validation at the network boundary.
- Test adapters with both accepted and rejected fixtures.
- Fail continuous integration when the committed contract and generated output diverge.
Enable strict compiler settings, especially null checking, and avoid broad assertions that bypass the boundary. Automation should eliminate repetitive work while preserving the review decisions that make the types useful.
Key takeaways
- Generate a draft, then verify optional fields, nullability, unions, and empty arrays against real contract evidence.
- Validate runtime data before components receive it; an interface or assertion cannot inspect a response.
- Keep transport objects separate from view models so formatting and fallback decisions remain testable.
- Store sanitized fixtures, test invalid inputs, and review regenerated changes as deliberately as application code.
- Prefer an existing authoritative schema when available, and make uncertainty visible instead of encoding guesses as guarantees.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor