Tag Archives: strictNullChecks

Migrating a codebase to enable strictNullChecks

Migrating a codebase to enable strictNullChecks can be tricky.

TypeScript’s strictNullChecks is a powerful compiler flag that enhances code safety by detecting potential null and undefined values at compile time.

There is some interesting discussions on migration, but to me none of them quite were satisfying:

I believe there is another incremental way

Let’s use this code as our example. With strictNullChecks : false it will not errors. With it strictNullChecks: true it will.

type User {
  email?: string
}

function getUserEmail(user: User): string {
  return user.email; // user.email might be null or undefined
}

Simple enough to fix. But in a large codebase we may have hundreds of these errors, and many will be much more complex. In my teams codebase, we had north of 500 errors and the count was unintentionally increasing.

Two Goals:

  • How might we incrementally resolve existing issue?
  • How might we prevent additional issues creeping in?

Enable strictNullChecks → Mark errors with @ts-expect-error → Setup eslint rule → Monitor with esplint

1. Enable strictNullChecks

Enable strictNullChecks is the natural first step in migrating. Adjust the compilerOptions flag for strictNullChecks in your tsconfig.json.

{
  "compilerOptions": {
    "strictNullChecks": true,
  }
}

By setting strictNullChecks to true, the TypeScript compiler will perform stricter checks on nullable values, reducing the possibility of null or undefined-related runtime errors.

2. Mark all existing errors with @ts-expect-error

There were likely be a large number of strictNullChecks exceptions in an existing codebase. Realistically, we probably can’t fix them all right away. We can use typescript’s @ts-expect-error comments before every instance of an error to temporarily suppress strictNullChecks errors per line.

function getUserEmail(user: User): string {
  // @ts-expect-error: 🐛 There is a null/undefined issue here which could cause bugs! Please fix me.
  return user.email;
}

This tells the typescript compiler that we’re aware of the error and currently expect it. We are marking them for further consideration during the migration process.

As an aside: @ts-expect-error is generally preferred over @ts-ignore. 
@ts-expect-error - Is temporary. Once the issue is fixed, typescript will remind us we can remove the @ts-expect-error. 
@ts-ignore - Is more permanent. suppresses the error and doesn't expect it to be fixed later.

At this point you could finish here!

However I recommend leveraging eslint to also keep us accountable.

3. Using eslint to highlight lines needing a refactor

While @ts-expect-error comments provide a temporary workaround, it’s important to gradually eliminate their usage to achieve the full benefits of strictNullChecks. Relying on @ts-expect-error extensively can undermine the benefits of type safety. We should flag these as not-ok in our code base. I would like to have a red or yellow squiggle marking them.

With eslint we can configured the @typescript-eslint/ban-ts-comment to warn on the usage of an @ts-comment. This further makes it clear in our editors that @ts-expect-error is temporary and should be fixed.

Example .eslintrc.json:

{
  "overrides": [
    {
      "files": ["*.ts", "*.tsx"],
      "rules": {
        "@typescript-eslint/ban-ts-comment": [
          "warn", {
            "ts-expect-error": true,
            "ts-ignore": true,
            "ts-nocheck": true,
            "ts-check": true
          }
        ]
      }
    }
  ]
}

4. Using eslint to discourage new issues

To take the enforcement of code quality a step further, we can introduce esplint—a tool that specializes in tracking and managing eslint warnings counts and enforcing that the count should only decrease. By leveraging esplint, we can keep a count of @ts-expect-error occurrences in our codebase. This count also serves as a metric to gauge progress during the migration. The goal is to steadily reduce the count, indicating a decreasing reliance on @ts-expect-error comments – thus an increase of strictNullChecks and an overall improvement in code quality.

Migrating a codebase to enable strictNullChecks

From here the codebase is ready to be slowly refactored. We encourage our team as they are working on a stories that touche code near one of these error, to take the time to refactor and cleanup the null checking.

This refactoring might involve implementing better error handling mechanisms, like using TypeScript’s union types, optional chaining (?.), or nullish coalescing operator (??).

Conclusion

Migrating a codebase to enable strictNullChecks can significantly improve code quality and enhance overall code quality. I believe by following the this pattern is a pragmatic and straightforward approach to enabling strictNullChecks. With diligent effort, we can all embrace strictNullChecks and enjoy the benefits of reduced runtime errors and write more code with confidence.

(Cover photo: White Sands National Park, New Mexico – Jonathan Stassen / JStassen Photography)