4.3 strictNullChecks: Why Null Was a Billion-Dollar Mistake
Right, let’s talk about the ghost in the machine. The concept of null was famously dubbed a “billion-dollar mistake” by its inventor, Tony Hoare. He’s not wrong. It’s the equivalent of handing you a beautifully wrapped box, you shaking it with anticipation, and then finding out it’s completely empty. Or worse, it’s a box that may or may not even exist. This ambiguity is the root of countless runtime errors, the infamous Cannot read property 'x' of null that has haunted web browsers since the dawn of time.
TypeScript, being the brilliant, pragmatic language that it is, looked at this mess and said, “We can do better.” But it had to be pragmatic. It couldn’t just delete null and undefined from the JavaScript universe. Instead, it gave us a superpower: the strictNullChecks compiler option. Turn this on, and your type system transforms from a friendly suggestion into a rigorous, logical proof engine. With it off, the type checker is basically wearing beer goggles: every type looks like it might also be null or undefined, so it just assumes everything will be fine. It’s not fine.
The Default: A World of Maybe-Nulls
When strictNullChecks is disabled (the default in old codebases, a crime against sanity in new ones), the type system lies to you. A string isn’t just a string. It’s a string, or null, or undefined. This means every time you access a property or call a function, you’re performing a potentially dangerous operation. The compiler just shrugs and says, “You probably know what you’re doing.”
// With strictNullChecks: false
let userName: string = getUserInput(); // Imagine this comes from a form
console.log(userName.toUpperCase()); // Runtime error if userName is null!
function getLength(s: string): number {
return s.length; // Seems safe, right? Wrong.
}
// This compiles happily, but will explode at runtime if passed null.
const length = getLength(null);
This is madness. We have a type called string, but it doesn’t actually mean “a string.” It means “a string, probably, maybe.” This is the billion-dollar mistake playing out in your code, every day.
Enabling strictNullChecks: The Truth Hurts (But Less Than a Runtime Error)
Flip the switch. Set strictNullChecks to true in your tsconfig.json. Suddenly, the type system becomes honest. A string is a string. If a value can also be null or undefined, you must say so explicitly using a union type: string | null.
The previous code now rightfully fails to compile:
// With strictNullChecks: true
let userName: string = getUserInput(); // Error! What if getUserInput returns null?
// Type 'string | null' is not assignable to type 'string'.
function getLength(s: string): number {
return s.length;
}
getLength(null); // Compiler Error: Argument of type 'null' is not assignable to parameter of type 'string'.
The compiler is no longer your enabling friend; it’s your brilliant, paranoid colleague who points out every single thing that could possibly go wrong. And you will thank them for it.
The Tools for the Job: Dealing with the Truth
So, your code is now covered in red squiggles. Excellent. This is progress. Now you have to handle the potential absence of a value, which is what you should have been doing all along. TypeScript gives you the tools to do this explicitly and safely.
1. Narrowing: The most common and idiomatic way. You check for the presence of a value, and the type checker is smart enough to understand what that means inside the block.
function printUppercase(text: string | null) {
if (text === null) {
// In this branch, text's type is narrowed to 'null'
console.log('No text provided');
return;
}
// In this branch, text's type is narrowed to 'string'. It's safe!
console.log(text.toUpperCase());
}
2. The “Bang” Operator (!): For When You’re Feeling Lucky
This is the postfix assertion operator. You’re telling the compiler, “I, the developer, know better than you. This value is definitely not null or undefined right now, even though the type says it might be.” Use this sparingly. It’s a sledgehammer. It’s great for quick scripts or situations where you have external guarantees, but it completely subverts the type safety you just worked so hard to get. It’s your “I know what I’m doing” cap, and you will wear it ironically.
let maybeNumber: number | null = getNumberFromLegacyAPI();
// We've done the check elsewhere, we promise.
const value = maybeNumber!.toFixed(2); // Compiler trusts you. Don't lie.
3. Providing a Default: Often the sanest choice.
const score: number | undefined = getCachedScore();
// Use the nullish coalescing operator (??) to provide a default
const displayScore = score ?? 0; // If score is null/undefined, use 0.
undefined vs. null: A Questionable Distinction
Here’s a rough edge, courtesy of JavaScript’s… let’s call it “organic” design. Why are there two ways to represent nothing? undefined typically means “a value hasn’t been assigned,” while null is an intentional “empty” assignment. In practice, this distinction is almost meaningless and causes endless friction. You’ll often find yourself writing string | null | undefined, which is a mess. My advice? Pick one (undefined is often more idiomatic in TS) and stick with it across your codebase. Consistency is your weapon against this particular absurdity.
The bottom line is this: enabling strictNullChecks is non-negotiable for any serious TypeScript project. It moves a whole category of devastating runtime errors into compile-time checks. It forces you to write more explicit, robust, and intentional code. It turns TypeScript from a fancy linter into a truly sound type system. It’s the single most important setting in your configuration. Turn it on, embrace the errors, and start writing code that doesn’t lie to you.