TotalWebTool

Type Safety Doesn't Prevent Bad Assumptions

Published May 18, 2026 by Editorial Team

Abstract editorial illustration of rigid type shapes colliding with messy runtime inputs, network uncertainty, and edge-case drift

Type safety is useful. It is not a truth machine.

That distinction matters because many teams quietly expect typed code to do more than it can. They adopt TypeScript, watch a class of mistakes disappear, and then let that success turn into a broader assumption: if the editor is green, the system is probably safe.

It is not.

TypeScript's own documentation says this plainly. In its handbook section on type compatibility, it notes that the language intentionally allows some operations that cannot be proven safe at compile time, which means the type system is not fully sound. That is not an accident or a flaw in documentation. It is part of the design tradeoff for making JavaScript code practical to type. (TypeScript Handbook: Type Compatibility)

So the real question is not whether typed code helps. It absolutely does.

The real question is what typed code can help with, what it cannot, and what site and application owners should do about the gap.

Typed Code Protects Models, Not Reality

A type declaration tells the compiler how your program intends to use a value. It does not force a browser, an API, a webhook, a CMS, a queue consumer, or a customer to obey that declaration at runtime.

That is why a typed codebase can still fail badly in production:

  • an API returns a shape that is technically valid JSON but semantically wrong for the page
  • a field exists in one environment but not another
  • a property is present with undefined when your logic assumes it is absent
  • an async workflow rejects, races, or partially succeeds
  • a narrow happy-path type hides an ugly edge case at the system boundary

None of those are contradictions of type safety. They are examples of confusing internal consistency with external truth.

Async Code Makes Bad Assumptions Expensive

Async code is one of the easiest places to overestimate what types are buying you.

MDN's async function and await references are a useful reminder of the runtime model: an async function always returns a Promise, and await will throw the rejection reason if that promise rejects. In other words, the type of the eventual success value does not cancel the possibility of failure, timeout, cancellation, or out-of-order completion. (MDN: async function, MDN: await)

A function like this looks reassuring:

async function getUser(): Promise<User> {
  const response = await fetch('/api/user');
  return response.json();
}

But the annotation does not guarantee:

  • that the server returned a 200
  • that the JSON shape actually matches User
  • that the request was not partially stale
  • that the caller handled retries, loading races, or rejected promises

It only guarantees what the codebase has chosen to claim.

TypeScript offers a clue here too. The useUnknownInCatchVariables option exists because blindly treating caught errors as a known shape is itself an unsafe assumption; with the flag enabled, catch variables default to unknown so code has to narrow them before use. (TSConfig: useUnknownInCatchVariables)

That is the deeper lesson: async code needs runtime discipline, not just typed signatures.

Nullability and Optional Data Still Need Aggressive Honesty

A large share of runtime bugs are not spectacular failures. They are smaller assumption failures around null, undefined, missing keys, and present-but-empty data.

TypeScript can help, but only if teams enable and respect the stricter checks. The strictNullChecks option makes null and undefined distinct types instead of letting them flow through the codebase as if they were ordinary values. The noUncheckedIndexedAccess option adds undefined to undeclared fields accessed through an index signature. And exactOptionalPropertyTypes distinguishes between a property being absent and a property being explicitly set to undefined, which matters because those two states behave differently at runtime. (TSConfig: strictNullChecks, TSConfig: noUncheckedIndexedAccess, TSConfig: exactOptionalPropertyTypes)

Those flags do not make the system perfect. But they do force teams to be more honest about where uncertainty already exists.

That honesty matters operationally for both sites and applications:

  • a missing price can break merchandising logic
  • an optional image can collapse layout or metadata generation
  • an undefined feature flag can expose the wrong interface
  • a nullable user field can break analytics attribution or checkout flow assumptions

These are product failures as much as code failures.

Boundaries Are Where Assumptions Become Incidents

The most important strategic point for owners is this:

the real risk is usually not inside the typed core of the application. It is at the boundaries where untrusted or drifting data enters the system.

OWASP's input validation guidance says validation should happen as early as possible in the data flow and should be applied to all potentially untrusted sources, not only browser input but also backend feeds, partner systems, suppliers, and other integrations. It also distinguishes between syntactic validation and semantic validation, which is exactly the distinction many typed systems skip. (OWASP: Input Validation Cheat Sheet)

That maps directly to modern web systems. Your weak points are often:

  • form submissions
  • query parameters
  • CMS content
  • webhook payloads
  • third-party APIs
  • environment variables
  • browser storage
  • background job payloads

TypeScript can describe what you want those inputs to look like. It cannot make them arrive that way.

What Smart Teams Should Do Instead

The answer is not to back away from typed code.

The answer is to stop treating it as the final layer of defense.

A stronger operating model looks like this:

1. Keep TypeScript, but raise the floor

If a team is going to depend on TypeScript strategically, it should depend on the stricter version of TypeScript, not the most permissive one. In practice that means at least taking strictNullChecks seriously, and usually pairing it with options such as noUncheckedIndexedAccess and exactOptionalPropertyTypes where the codebase can tolerate the discipline. (TSConfig: strictNullChecks, TSConfig: noUncheckedIndexedAccess, TSConfig: exactOptionalPropertyTypes)

2. Validate every trust boundary at runtime

Parse external data into trusted internal shapes instead of declaring trust by annotation. That can mean schema validation, explicit parsing functions, allowlists, range checks, semantic checks, or all of the above depending on the risk surface. OWASP's guidance is clear that validation should happen early and on all untrusted inputs. (OWASP: Input Validation Cheat Sheet)

A minimal pattern looks like this:

type User = {
  id: string;
  email: string;
};

function parseUser(value: unknown): User {
  if (!value || typeof value !== 'object') {
    throw new Error('Invalid user payload');
  }

  const input = value as Record<string, unknown>;

  if (typeof input.id !== 'string' || typeof input.email !== 'string') {
    throw new Error('Invalid user payload');
  }

  return {
    id: input.id,
    email: input.email,
  };
}

That is less elegant than pretending response.json() as User is enough. It is also much more defensible.

3. Design for degraded states, not just success states

If async work can reject, then product flows need explicit error, timeout, retry, and partial-data behavior. If optional values can be missing, templates and components need defined fallback behavior. Typed code does not replace resilient state design; it makes the missing design more obvious when teams choose to look.

4. Track runtime truth in production

Owners should care about this because the real cost of bad assumptions is not a red squiggle. It is broken checkout, blank account pages, silent analytics corruption, malformed emails, failed imports, or content rendering regressions in production.

That means the strategy cannot stop at "we use TypeScript." It should include:

  • boundary validation
  • contract tests for integrations
  • structured error logging
  • release checks for null and empty-state handling
  • production telemetry around failed parses, rejected async work, and fallback-path usage

5. Treat types as a leverage tool, not a guarantee

The real business value of a typed codebase is still substantial. It improves refactoring safety, clarifies shared models, catches many categories of mistakes early, and makes complex systems easier to change. But those benefits are strongest inside the codebase, where your team controls the invariants.

Once the system touches networks, humans, vendors, content editors, browsers, and asynchronous timing, the problem shifts from type design to operational trust management.

What This Means for Site and Application Owners

If you own a marketing site, product site, SaaS application, or internal platform, the strategic takeaway is simple:

keep typed code, but budget for runtime uncertainty on purpose.

In practice:

  • use TypeScript to make internal models and refactors safer
  • enforce stricter compiler options where the team can sustain them
  • validate any data crossing a boundary before it becomes trusted state
  • build interfaces that can survive missing, delayed, or malformed data
  • monitor production for the assumptions the type system cannot verify

That is how you keep the benefits of typed code without turning those benefits into false confidence.

Bottom Line

Type safety does not prevent bad assumptions.

It prevents some classes of bad assumptions, inside a controlled model of the program, before runtime.

That is still enormously valuable. But it is not enough on its own for applications that depend on async work, optional data, third-party systems, user input, or real-world edge cases.

The teams that get the most out of typed code are usually not the teams that trust it blindly.

They are the teams that pair it with validation, stricter nullability discipline, resilient interface design, and production feedback loops.

Sources

Share this article

Return to Blog