TypeScript Types Lie at Runtime: Why Prod Still Crashes

Your code is fully typed but production still throws undefined errors. Types vanish at runtime. Learn where they lie and how to validate boundaries with Zod and stricter compiler flags.

TypeScript types are erased at compile time, so they cannot protect you from data that arrives at runtime from APIs, databases, forms, or storage; production still crashes with undefined errors unless you validate those boundaries and tighten a few compiler options.

If you have ever shipped code with zero type errors and still seen Cannot read properties of undefined, this is why.

Where Types Lie

Boundary What TypeScript believes What can actually happen
res.json() Returns any, then you annotate it API returns a new shape, a null, or an error object
as User assertions You are right You are wrong, and nothing checks
JSON.parse any Corrupt or old localStorage data
Database results Matches your interface Column nullable, migration not run
array[0] Always T undefined on an empty array
process.env.X string undefined when missing
Third-party webhooks Documented payload Extra, missing, or renamed fields

Fix 1: Validate at Every Boundary

Parse unknown data into a type instead of asserting it.

import { z } from 'zod'

const User = z.object({
  id: z.string(),
  email: z.string().email(),
  plan: z.enum(['free', 'pro']).default('free'),
})
type User = z.infer<typeof User>

const res = await fetch('/api/me')
const parsed = User.safeParse(await res.json())
if (!parsed.success) {
  reportError(new Error('Invalid /api/me response'), { issues: parsed.error.issues })
  return null
}
const user: User = parsed.data

The type now matches what was checked, so the compiler and the runtime agree. The Zod validation guide covers request bodies in detail, and the logging here means an API contract change shows up as a clear error rather than a crash three components later.

Fix 2: Turn On the Strict Flags That Catch Real Bugs

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "useUnknownInCatchVariables": true
  }
}

noUncheckedIndexedAccess makes array[0] typed as T | undefined, which forces the null check that would have prevented the crash. It is the single most useful flag beyond strict. Expect a wave of new errors on first enabling it; fix them gradually.

Fix 3: Ban Unsafe Assertions in Lint

Use typescript-eslint rules such as no-explicit-any, no-unsafe-assignment, and no-unsafe-member-access. Treat as X on external data as a code review flag.

Fix 4: Validate Environment Variables at Boot

A missing variable is typed as string but is undefined at runtime. Parse process.env through a schema when the app starts; see env vars undefined in production.

Fix 5: Make Null Explicit in Your Data Layer

Generate types from the source of truth (Prisma, Supabase types, OpenAPI) instead of hand-writing interfaces. Hand-written types drift. For database failures that types cannot prevent, such as unique violations, see Prisma errors in production.

What Types Still Cannot Do

Even perfect types will not stop timeouts, network failures, race conditions, or logic bugs. You still need runtime visibility. Catching the leftovers is the job of monitoring; the classic one is TypeError: Cannot read properties of undefined, and a systematic approach is in Node.js production error handling.

Let Relia Close the Gap

When a runtime shape mismatch slips past the compiler, Relia catches the error in production and writes the incident up in plain words: what happened, why it thinks so (with a confidence level), and one thing to do next. On request it writes a fix plan, such as a schema check or null guard at the unvalidated boundary, with a "Copy for your AI assistant" button; Relia doesn't change code itself.

FAQ

Why does TypeScript not catch undefined errors?

It checks code against declared types, but it cannot verify that runtime data matches them. Assertions and any bypass it entirely.

Is as always bad? Not always, but never use it on external data. Parse and validate instead.

Does Zod slow down my app?

Overhead is small compared to network and database time. Validate at boundaries, not in hot inner loops.

All guides