Strict Null Checks in TypeScript: What the Compiler Won't Tell You and Where It Actually Hurts in Production
I was reviewing a Server Action in Next.js — something that compiled without a single error, clean types, green lint — when a Cannot read properties of undefined (reading 'id') hit in runtime. Three minutes of retrospective later I understood the problem: the compiler had given me the green light and I believed it. That was a mistake.
My thesis, straight up: strict null checks is necessary but not sufficient. The TypeScript compiler is the first filter in the system, not the last. Real null safety comes from runtime validation at the edges of the system — and there are four concrete patterns where the compiler says OK and production says otherwise.
This isn't a "turn on strict: true and you're done" post. It's a map of where the compiler fails silently, using the Next.js 16 + Prisma ORM 5 + strict TypeScript stack as a concrete reference.
Strict Null Checks in TypeScript Production: What the Flag Actually Activates
When you enable strict: true in tsconfig.json, TypeScript turns on a more restrictive set of checks. According to the is the tool that fits best in this stack:
// ✅ Validation with Zod at the external data entry point
import { z } from "zod";
const ConfigSchema = z.object({
timeout: z.number().positive(),
endpoint: z.string().url(),
});
async function getConfiguration() {
const raw = await fs.readFile("config.json", "utf-8");
const parsed = JSON.parse(raw);
return ConfigSchema.parse(parsed); // throws ZodError if shape doesn't match
}
// Now the inferred type is exactly { timeout: number; endpoint: string }
// and runtime guarantees the shape before the data reaches the rest of the code
const config = await getConfiguration();
const ms = config.timeout * 1000; // safe
The same pattern applies to Server Actions in Next.js that receive form data, to external API responses, and to any data that crosses the system boundary.
Common Mistakes When Configuring Strict Null Checks
Three mistakes show up constantly when teams enable strict on an existing codebase:
1. Turning off individual checks to make it compile
// ❌ This defeats the entire purpose of strict
{
"compilerOptions": {
"strict": true,
"strictNullChecks": false
}
}
If a check breaks too much existing code, the right path is to migrate progressively with annotated and dated // @ts-expect-error comments — not to disable the flag globally.
2. Using the non-null assertion operator (!) without a real guard
// ❌ The ! operator tells the compiler "trust me"
// but does zero verification at runtime
const name = user!.name; // TypeError if user is null
Every ! in the codebase is potential technical debt. If you see more than five ! in a single file, that's a signal that the types aren't accurately modeling the domain's reality.
3. Confusing strict in Next.js config with strict in tsconfig
next.config.js has a typescript.ignoreBuildErrors option that, when set to true, completely bypasses the compiler during the build. The strict in tsconfig.json means nothing if the build never fails on type errors.
Decision Checklist: Where to Validate and Where to Trust the Compiler
Before deciding whether to add runtime validation or trust the static type, run through this checklist:
| Question | Yes | No |
|---|---|---|
| Does the data come from outside the process? (API, file, DB, form) | Validate with Zod | Compiler is enough |
Does the library have any types or outdated @types/? | Add explicit guard | Compiler is enough |
| Are you using custom assertion functions? | Verify they throw | — |
Is the Prisma relation in the include? | Type is precise | Add defensive guard |
Does the type use ! to suppress a null? | Revisit the domain model | — |
Rule of thumb: if the data crossed a system boundary (network, disk, form, environment variable), validate at runtime. If the data is internal to the process and the type was inferred by TypeScript, the compiler is enough.
Limits of This Guide
What you can't conclude from this post without more evidence:
- How many production bugs come from each pattern — that depends on the specific codebase, test coverage, and team maturity.
- Whether Zod is always the best option over alternatives like — there are bundle size and ergonomics trade-offs that deserve their own analysis.
- Whether these patterns apply equally in a codebase using tRPC or GraphQL with codegen — those systems have their own validation layers that change the equation.
What you can conclude: the four patterns are reproducible, have concrete solutions, and apply directly to the Next.js 16 + Prisma 5 + strict TypeScript stack.
FAQ — Strict Null Checks TypeScript Production
With strict: true enabled, can I trust there are no nulls at runtime?
No. strict: true guarantees the compiler warns you when a type can be null or undefined — but it can't verify data coming in from outside the process. Data from APIs, forms, files, and databases needs additional runtime validation.
Does Prisma ORM generate types that exactly reflect what each query returns?
Partially. Prisma 5 infers the type from the schema and from the query's include/select. If you don't include a relation, the field won't be on the returned object — but the generated type may not express that with enough precision in all cases. The safe practice is to always make the include match what downstream code consumes.
When does it make sense to use // @ts-expect-error instead of properly fixing the type?
Only in two cases: when you're progressively migrating a legacy codebase to strict (annotated with a comment explaining why and an expected resolution date), or when you're deliberately testing an error. In stable production code, @ts-expect-error without justification is technical debt with an unknown expiry date.
Does JSON.parse always return any?
Yes, by design. TypeScript can't know the shape of the JSON until runtime. The only way to recover a concrete type is to validate the result with a library like , all touch on the difference between what the system promises and what it delivers.
Original sources:
- TypeScript Handbook — Strict Mode:
This article was originally published on juanchi.dev
SOCIAL SHARE CARD GENERATOR