Here is a line of code that appears in almost every TypeScript frontend I have worked on, including ones I wrote:
const user = (await response.json()) as UserFrom that line onward, the editor is confident. Autocomplete works. Renaming a field updates every usage. The build passes. And none of it means anything, because as User is not a check — it's an instruction to stop checking. response.json() returns any, TypeScript's types are erased before the code ever runs, and the only thing that assertion has established is that nobody will be warned when the server sends something else.
This is fine right up until the API is written in a language with different opinions about types than TypeScript has. I spend my days on exactly that seam — a typed frontend talking to a PHP commerce backend through a Node API layer — and the failures are consistent enough to be worth writing down.
The four ways the types stop being true
Numbers arrive as strings. This is the classic one. A lot of PHP database access returns every column as a string unless it's been configured otherwise, and json_encode faithfully passes that along. So price is typed number, contains "19.99", and your total comes out as "19.99" + "4.50" — which JavaScript is happy to evaluate as "19.994.5" without raising so much as a warning. The interface said number. The runtime disagreed. Nothing caught it.
Empty arrays become empty objects, or the reverse. PHP has one array type doing the work of both a list and a dictionary, and json_encode guesses which one you meant from the keys. An empty PHP array encodes as []. The same array with one string key encodes as an object. So an endpoint returning a list of applied discounts sends [] when there are none and {"SAVE10": {...}} when there are — and your discounts.map() works perfectly in every test you wrote against a store with no discounts.
Missing and null are not the same, and both mean "absent". TypeScript makes a real distinction between field?: string and field: string | null. Most backends do not, and will cheerfully send you a key that's absent on Monday and null on Tuesday depending on which code path produced it. If your rendering does user.company.length, one of those days is an exception in production.
Dates are strings pretending otherwise. JSON has no date type. So createdAt is typed Date in the interface, is actually a string, and works for months because every place you touch it happens to be rendering it. Then someone sorts by it, or subtracts two of them, and the bug is three layers away from the cast that caused it.
Validate at the boundary, once
The fix is not to litter type guards through the application. It's to decide where untrusted data becomes trusted data, and make that one place actually earn it.
Concretely: no component, hook or store ever calls fetch and casts. Each resource gets one function that performs the request, checks the response against a schema at runtime, and returns a value whose type is derived from that schema rather than declared alongside it. Everything past that function can treat the data as genuinely typed, because something verified it. Everything before it is explicitly untrusted.
Whether you use a validation library or hand-write the parse functions matters much less than that the type and the check come from the same definition. The failure mode you're avoiding is an interface that says one thing and a validator that says another — at which point you have two contracts and no way to know which one production is following.
The boundary is also the right place to normalise. If the backend sends prices as strings and there's no realistic path to changing that, convert them there — once, visibly, in the layer whose job is to translate. What you must not do is let a numeric string through and handle it defensively in fifteen call sites, which is how you end up with Number(price) scattered across a codebase and no clarity about which fields need it.
Model what you consume, not what they store
There's a strong pull toward making your frontend types a faithful mirror of the backend's data model — same names, same nesting, same everything. It feels rigorous. It's usually a mistake, because it couples your UI to their schema and imports every historical oddity in it: the field that's named after a feature that was renamed two years ago, the flag that only means something in combination with another flag, the nested object that exists because of how it's stored rather than how it's used.
The boundary layer is where that translation should happen. Downstream code works with the shape the interface actually needs; the parse function absorbs the difference. When the backend renames a column, one file changes.
This is essentially what I've built on the server side too — an API layer in front of a commerce platform whose internal schema is shaped for its own admin rather than for anyone consuming it. Same principle at a different altitude: normalise at the edge so that everything downstream can be simple.
Errors are part of the contract
One more thing that reliably gets skipped: fetch does not reject on a 404 or a 500. It resolves, and then .json() either parses an error body into something that isn't the shape you expected, or throws on an HTML error page from a proxy that never reached your application at all.
So the same boundary function that validates the success shape has to check the status first, and needs a defined answer for a body that isn't JSON. Not because it's elegant, but because "the API returned a 502 and the page rendered undefined everywhere" is a genuinely common production bug with a genuinely boring fix.
What this actually buys you
Not correctness in the abstract. Something narrower and more useful: when the backend changes shape, you find out at the boundary, with a message naming the field, instead of three components deep with undefined is not an object.
TypeScript is excellent at keeping a codebase honest with itself. It cannot keep a codebase honest about someone else's server — that part is a runtime problem, and it needs a runtime answer. One layer, one place, and then you can go back to trusting your types everywhere else.