TypeScript non-nullable types and optional chaining are your best defense against runtime null pointer errors. Learn how to stop crashing in production today.
I remember the exact moment I stopped fearing the "Cannot read property of undefined" error. It was during an on-call rotation for a high-traffic dashboard where we were pushing updates every few hours. We kept hitting production crashes because our API responses were occasionally missing nested fields, and our frontend code blindly accessed them. Switching to strict null checks was the turning point that saved us roughly 15 hours of debugging time over the following month.
If you aren't running with strictNullChecks: true in your tsconfig.json, you’re essentially flying blind. Without it, null and undefined are effectively ignored by the compiler, meaning any variable can be anything at any time. When you enable this flag, TypeScript forces you to acknowledge that a value might be missing.
Initially, this feels like you’re fighting the compiler. You’ll see red squiggles everywhere. But that friction is actually the sound of your code becoming safer. It’s the difference between guessing what your data looks like and knowing it.
Before I learned to lean on modern syntax, I spent way too much time writing verbose guard clauses. I used to do this:
TYPESCRIPT// The old way: verbose and error-prone if (user && user.profile && user.profile.settings) { console.log(user.profile.settings.theme); }
This is defensive programming at its most tedious. With optional chaining (?.), introduced in TypeScript 3.7, we can simplify this drastically. It’s cleaner, safer, and much easier to read:
TYPESCRIPT// The modern way: clean and safe console.log(user?.profile?.settings?.theme);
This approach provides immediate runtime safety by short-circuiting to undefined if any part of the chain is nullish. It’s a small change, but it eliminates the most common category of production crashes in JavaScript applications.
Sometimes, you know for a fact that a value exists, but the compiler remains skeptical. This is where NonNullable<T> becomes a lifesaver. It’s one of the TypeScript utility types you will reach for weekly because it explicitly strips null and undefined from a type.
Consider a scenario where you're filtering an array of potential results:
TYPESCRIPTtype Result = string | null | undefined; const rawResults: Result[] = [CE9178">'success', null, CE9178">'error', undefined]; // The filter alone isn't enough for TS to infer the type const cleanResults = rawResults.filter(r => r !== null && r !== undefined); // We can refine this using NonNullable const filteredResults: NonNullable<Result>[] = rawResults.filter( (r): r is NonNullable<Result> => r != null );
By using a type predicate combined with NonNullable, you tell the compiler exactly what’s happening. This is much more robust than using as string type assertions, which essentially lie to the compiler and defeat the purpose of using TypeScript in the first place.
I once tried to apply this logic to a complex configuration object, but I ran into issues where I had nested nulls that were actually semantically valid. I first tried to just use Partial<T>, but that made the entire object optional, which was too loose.
I eventually switched to a combination of mapped types and strict null checks to ensure that only specific, nullable fields were handled. If you are dealing with domain entities, you might also consider implementing TypeScript Branded Types: Enforcing Domain Integrity at Compile-Time to ensure that even once a value is "non-nullable," it still conforms to your specific business rules.
Q: Should I use ! (non-null assertion operator) to bypass errors?
A: Use it sparingly. It’s a "trust me" signal to the compiler. If you find yourself using it everywhere, your type definitions probably don't reflect your actual data structure. Refactor the types instead.
Q: Is optional chaining enough to replace all null checks?
A: It’s great for reading properties, but you still need logic to handle the undefined case if your UI requires a fallback value. Use the nullish coalescing operator (??) to provide defaults: user?.name ?? 'Anonymous'.
Q: How do I handle APIs that return null when they should return an empty array?
A: This is a classic backend-to-frontend mismatch. I prefer creating a "normalization" layer at the API boundary that maps null to [] before the data touches the rest of your application.
I’m still refining how I handle deep, nested objects from third-party APIs. While NonNullable works great for flat structures, deep objects often require recursive types to truly enforce safety. I’m currently experimenting with schema validation libraries like Zod, which bridge the gap between runtime data and compile-time types more effectively than manual type definitions.
Don't treat these tools as just "more code." Treat them as a way to build a system that tells you when you're wrong before your users do. It's a shift in mindset: stop defending against the unknown, and start defining the known.
Generics in TypeScript can feel like an academic hurdle, but they pay off when you use them to enforce type safety in API calls and reusable components.
Read more