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.