Skip to content

Engineering Hygiene

TypeScript Won't Save You From Your API

Your types at the API boundary are a description of last week's schema, not a guarantee about today's. Most teams have end-to-end type theater, and the gap only shows up at runtime.

By Anshul Kapoor·July 2026·5 min read
TypeScriptAPI DesignType SafetyFrontend
LinkedIn

TL;DR

  • TypeScript verifies that your code agrees with your types. It cannot verify that your types agree with reality. At the API boundary, those are different claims.
  • Generated types drift. Codegen describes the schema it last saw, and the backend does not wait for your build to change.
  • The compile-time green checkmark creates confidence exactly where you deserve it least: at the edge of the system you do not control.
  • Closing the gap takes runtime validation at the boundary, codegen wired to the live schema in CI, and honest nullability. Pick all three, not one.

The confidence machine

TypeScript is very good at its job. Inside your codebase, it catches renamed properties, wrong argument orders, missing null checks, the whole class of bugs that used to eat afternoons. Teams that adopt it rarely go back. The confidence is real.

But TypeScript makes one specific promise, and it is narrower than most people internalize: your code is consistent with your type declarations. That is the entire contract. If the declarations are wrong, the compiler will happily verify your code against a fiction, and it will do so with the same green checkmark it uses for the truth.

Inside your own codebase, declarations are rarely wrong, because the compiler derives them from the code itself. At the API boundary, the situation inverts. The types describing your backend's responses were written by hand, or generated from a schema at some point in the past. Either way, they are a claim about a system the compiler cannot see.

The compiler checks your code against the claim. Nothing checks the claim against the API.

TypeScript verifies that your code agrees with your types. Nothing verifies that your types agree with your API.

Where the types lie

The most common setup I see looks airtight on paper. The backend exposes a schema, GraphQL or OpenAPI. A codegen step turns it into TypeScript types. The frontend consumes those types everywhere. End-to-end type safety, says the architecture diagram.

Here is what actually happens. The codegen ran against the schema as it existed when someone last ran it. Maybe that is this morning. Maybe it is three weeks ago, because codegen output is committed to the repo and only regenerated when someone remembers. Meanwhile the backend keeps shipping. A field gets renamed. A nullable field becomes non-nullable, or worse, the reverse. An enum grows a new case.

Your types say everything is fine. Your build is green. Your tests pass, because your mocks were built from the same stale types. Every layer of your safety net is checking your code against the same outdated snapshot, agreeing with each other and disagreeing with production.

Then the response arrives at runtime with a shape your types swore was impossible, and the error surfaces three components downstream from the actual boundary, as a cannot-read-property in code that did nothing wrong.

Every layer of the safety net checks your code against the same stale snapshot. They all agree with each other and disagree with production.

A field rename walks into production

A concrete version, assembled from incidents I have watched more than once.

The backend team renames displayName to legalName on a customer object, for a good compliance reason. They deprecate the old field, add the new one, and announce it in a channel the frontend team mutes. Codegen on the frontend has not run in two weeks. The frontend keeps reading displayName, the types keep saying it exists, and for a while it even works, because the deprecated field is still being served.

Then the backend completes the deprecation and removes the field. No frontend build breaks, because no frontend code changed. No test fails, because the mocks still have displayName. The first signal anyone gets is customer names rendering as blank strings in production, and the on-call engineer spends an hour looking for the bug in the rendering code, because that is where the symptom is.

Notice what did not fail: the type system. It did exactly what it was designed to do, verify code against declarations. The declarations were simply two weeks out of date. That is not a TypeScript flaw. It is a workflow flaw wearing TypeScript's reputation as cover.

Type theater vs type safety

I call this pattern type theater: the organization has all the visible artifacts of type safety, generated types, strict mode, no-any lint rules, and none of the runtime guarantees the artifacts imply.

Type theater is worse than no types at the boundary, in one specific way. With no types, engineers treat API responses as untrusted and check things defensively. With stale types, engineers extend exactly the trust the types tell them to. The declared shape becomes an assumption baked into every component that touches the data. The failure, when it comes, is further from the boundary and harder to trace.

The tell for type theater is a question: "If the backend changed a field name right now, what is the first thing in your pipeline that would notice?" If the honest answer is "a user," you have theater. The checkmark is real. The safety is not.

If the backend renamed a field right now, what is the first thing in your pipeline that would notice? If the answer is "a user," you have type theater.

What actually closes the gap

Three practices, and they compound. Teams that do one get some protection. Teams that do all three stop having this class of incident.

First, validate at the boundary, at runtime. Parse API responses through a schema validator at the single place where data enters the app, and derive your TypeScript types from those schemas instead of writing them separately. Now the type is not a claim about the API. It is a description of what validation actually enforced. When the backend changes shape, you get one clear error at the boundary, naming the field, instead of undefined leaking into your component tree. Yes, validation has a runtime cost. It is small, it is at the edge, and it is a fraction of the cost of one production incident.

Second, regenerate types against the live schema in CI, not on a developer's laptop. Every pipeline run fetches the current schema and regenerates. If the output differs from what is committed, the build fails and says so. This turns "the backend changed something" from a silent drift into a visible diff, on the day it happens.

Third, be honest about nullability. Most API type definitions are optimistic: fields marked required because they are usually there. Every one of those is a small lie the compiler will repeat with confidence. If the backend can omit it, the type says optional, and the unwrapping happens visibly, at the boundary, where the decision belongs.

None of this replaces TypeScript. It finishes the job TypeScript starts. The compiler covers everything inside the walls. These practices cover the gate.

Trust, but verify at the edges

The general principle is older than TypeScript: static guarantees end where your control ends. Your types are law inside your repo, and hearsay about everything outside it. The network boundary, the third-party SDK, the localStorage blob written by last year's version of your app: every one of these is a place where the real shape of data is decided by something the compiler cannot see.

Strong teams draw that line deliberately. Inside it, they lean on the compiler completely and skip redundant runtime checks. At the line, they validate everything, once, and let clean types flow inward from there. The discipline is not "trust the types" or "distrust the types." It is knowing exactly where the types stop being knowledge and start being hope.

TypeScript will not save you from your API. It was never supposed to. It saves you from yourself, inside the walls you own, and it does that superbly. The edges are still your job.

Static guarantees end where your control ends. Types are law inside your repo, and hearsay about everything outside it.

Found this useful?

Share it with someone who might be wrestling with the same problem.

ShareLinkedIn

Thanks for reading.

If this resonated and you're hiring for Senior/Staff Frontend roles, I'd love to chat.