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

  1. 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 undefined or null before 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 if statement acts as a type guard. Inside the if block, TypeScript knows user.name is definitely a string (because it’s truthy and not undefined), allowing .toUpperCase() to be called safely.
  2. Function Parameters:

    • Diagnosis: Errors indicating a function expects a type but receives undefined or null, or a function parameter is declared as Type but might be undefined.
    • Cause: A function’s parameter is expected to be a specific type, but it could be null or undefined based on the calling code or default behavior.
    • Fix: Explicitly allow null or undefined in 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 that userName can indeed be these values. The if (userName) check then safely narrows the type within its scope.
  3. 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 be null or undefined at 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.
  4. Array Elements:

    • Diagnosis: Errors when accessing elements of an array, e.g., myArray[0] is Type 'T | undefined'.
    • Cause: Array methods like .find() or accessing an index might return undefined if 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 if statement acts as a type guard, ensuring firstEven is treated as a number within the if block.
  5. Type Assertions:

    • Diagnosis: Type 'string | null' is not assignable to type 'string'.
    • Cause: You’re trying to assign a value that could be null or undefined to 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 of str to string within that block. The ! assertion tells TypeScript to assume str is not null after the function call, similar to how it works with element access.
  6. Third-Party Libraries (DefinitelyTyped):

    • Diagnosis: Errors related to types from libraries that don’t have strict null checks enabled in their @types packages.
    • Cause: The library’s type definitions might not be accounting for null or undefined where your strictNullChecks expects them to.
    • Fix: Update your @types packages. 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.ts files) ensures that the library’s API is accurately represented, including potential null or undefined returns, allowing your TypeScript compiler to check them correctly.

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.

Want structured learning?

Take the full Typescript course →