Migrating to strictNullChecks in TypeScript is less about fixing a bug and more about leveling up your code’s safety net. The most surprising thing about strictNullChecks is that it doesn’t actually prevent null or undefined from existing in your JavaScript; it simply forces you to acknowledge and handle their potential presence at compile time.
Let’s see it in action. Imagine a simple function that might receive a user object, but the user’s name might be missing.
Without strictNullChecks, this code compiles fine:
interface User {
name?: string;
}
function greet(user: User) {
console.log(`Hello, ${user.name.toUpperCase()}!`);
}
const user1: User = { name: "Alice" };
greet(user1); // Works fine
const user2: User = {};
greet(user2); // Runtime Error: Cannot read properties of undefined (reading 'toUpperCase')
The problem here is that user.name could be undefined, and calling .toUpperCase() on undefined crashes your application. strictNullChecks makes TypeScript catch this before you run the code.
To enable strictNullChecks, you need to add "strictNullChecks": true to your tsconfig.json file.
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"strict": true, // This enables all strict type-checking options, including strictNullChecks
"strictNullChecks": true, // Explicitly enabling strictNullChecks (redundant if "strict" is true, but good for clarity)
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
After enabling it, the previous greet function will immediately produce a TypeScript error:
Property 'name' does not exist on type '{}'. Did you mean 'name?: string | undefined'?ts(2578)
Or, if the name property is declared but optional:
Object is possibly 'undefined'.ts(2551)
This is where the real work begins. You need to adjust your code to handle these null or undefined possibilities.
Common Causes and Fixes
-
Optional Properties:
- Diagnosis: TypeScript errors like
Object is possibly 'undefined'on properties marked with?. - Cause: You’re trying to access a property that might not be present without checking.
- Fix: Add a check for
undefinedornullbefore accessing the property.function greet(user: User) { if (user.name) { // Checks if user.name is truthy (not null, undefined, '', 0, false) console.log(`Hello, ${user.name.toUpperCase()}!`); } else { console.log("Hello, mysterious stranger!"); } } - Why it works: The
ifstatement acts as a type guard. Inside theifblock, TypeScript knowsuser.nameis definitely astring(because it’s truthy and notundefined), allowing.toUpperCase()to be called safely.
- Diagnosis: TypeScript errors like
-
Function Parameters:
- Diagnosis: Errors indicating a function expects a type but receives
undefinedornull, or a function parameter is declared asTypebut might beundefined. - Cause: A function’s parameter is expected to be a specific type, but it could be
nullorundefinedbased on the calling code or default behavior. - Fix: Explicitly allow
nullorundefinedin the parameter type, and handle it within the function.interface User { name?: string; } // Modified to accept null or undefined name function greet(user: User, userName: string | null | undefined = user.name) { if (userName) { console.log(`Hello, ${userName.toUpperCase()}!`); } else { console.log("Hello, guest!"); } } const user1: User = { name: "Alice" }; greet(user1); // "Hello, ALICE!" const user2: User = {}; greet(user2); // "Hello, guest!" greet(user1, null); // "Hello, guest!" - Why it works: By unioning the type with
| null | undefined, you tell TypeScript thatuserNamecan indeed be these values. Theif (userName)check then safely narrows the type within its scope.
- Diagnosis: Errors indicating a function expects a type but receives
-
Non-null Assertion Operator (
!):- Diagnosis:
Object is possibly 'undefined'errors where you are certain the value won’t be null/undefined at runtime. - Cause: You’ve determined through external logic or prior checks that a value must exist, but TypeScript can’t infer it.
- Fix: Use the non-null assertion operator (
!).function getElement(selector: string): HTMLElement | null { return document.querySelector(selector); } // Without !, this would error if element could be null const myElement = getElement('#my-div'); // Use '!' to assert that myElement is not null myElement!.style.color = 'red'; - Why it works: The
!tells the TypeScript compiler, "Trust me, this value will not benullorundefinedat this point." Use this sparingly, as it bypasses compile-time safety. It’s best used when you’ve performed runtime checks that TypeScript can’t see.
- Diagnosis:
-
Array Elements:
- Diagnosis: Errors when accessing elements of an array, e.g.,
myArray[0]isType 'T | undefined'. - Cause: Array methods like
.find()or accessing an index might returnundefinedif no element matches or the index is out of bounds. - Fix: Check if the element exists.
const numbers = [1, 2, 3]; const firstEven = numbers.find(num => num % 2 === 0); // Type of firstEven is number | undefined if (firstEven !== undefined) { console.log(`First even number is ${firstEven * 2}`); } else { console.log("No even numbers found."); } - Why it works: Similar to optional properties, the
ifstatement acts as a type guard, ensuringfirstEvenis treated as anumberwithin theifblock.
- Diagnosis: Errors when accessing elements of an array, e.g.,
-
Type Assertions:
- Diagnosis:
Type 'string | null' is not assignable to type 'string'. - Cause: You’re trying to assign a value that could be
nullorundefinedto a variable that cannot be. - Fix: Either adjust the target variable’s type or use a type assertion if you’re absolutely sure.
function getPotentiallyNullString(): string | null { return Math.random() > 0.5 ? "hello" : null; } const str = getPotentiallyNullString(); // Incorrect: str could be null // const definiteString: string = str; // Correct: Handle the null case if (str !== null) { const definiteString: string = str; console.log(definiteString.toUpperCase()); } // Or, if you're *sure* it won't be null (use with caution) const definiteStringAsserted: string = getPotentiallyNullString()!; console.log(definiteStringAsserted.toUpperCase()); - Why it works: The
if (str !== null)check narrows the type ofstrtostringwithin that block. The!assertion tells TypeScript to assumestris notnullafter the function call, similar to how it works with element access.
- Diagnosis:
-
Third-Party Libraries (DefinitelyTyped):
- Diagnosis: Errors related to types from libraries that don’t have strict null checks enabled in their
@typespackages. - Cause: The library’s type definitions might not be accounting for
nullorundefinedwhere yourstrictNullChecksexpects them to. - Fix: Update your
@typespackages. If the issue persists, you might need to use type assertions (!) or wrap the library calls in your own type guards. Sometimes, a library’s types might be outdated or not fully conformant to strict null checks.npm install --save-dev @types/some-library # or yarn add --dev @types/some-library - Why it works: Updating the type definitions (
.d.tsfiles) ensures that the library’s API is accurately represented, including potentialnullorundefinedreturns, allowing your TypeScript compiler to check them correctly.
- Diagnosis: Errors related to types from libraries that don’t have strict null checks enabled in their
Migrating to strictNullChecks is a significant step towards more robust JavaScript. It forces you to confront the implicit assumptions your code makes about values always being present. The initial effort can feel tedious, but it pays off by eliminating a large class of common runtime errors.
The next hurdle you’ll likely face after getting strictNullChecks working is dealing with any types and their interactions with strict checks, which can introduce a new set of subtle errors if not managed carefully.