21.8 Adopting Strict Mode Incrementally
Right, so you’ve seen the light and want to turn on strict mode and its more discerning cousin, strictNullChecks. Good. But you’re not working in a greenfield project where you can just flip the switch and watch the world burn. You’re in the real world, with a codebase that has history, and that history is probably littered with any types and variables that are definitely null half the time. Trying to enable this for your entire project in one go is a recipe for 10,000 errors and a mutiny from your team. Let’s do this incrementally, like adults.
The key insight is that TypeScript’s compiler options can be controlled on a file-by-file basis using comments. This is our escape hatch. It lets us surgically apply stricter rules to new code or refactored code, while the legacy stuff continues to hum along under the old, more permissive rules. We’re going to quarantine the old code until we can fix it.
The Magic Comments: // @ts-strict and Friends
You don’t have to configure this in the tsconfig.json for your entire project. Instead, you can slap a comment at the top of a single file to enforce a specific rule. The most powerful one for our purposes is // @ts-strict, which is equivalent to enabling all strict mode flags for just that file. But you can be more granular.
// @ts-strict
// OR
// @ts-nocheck
// @ts-check
// @ts-ignore
// More granular control for the strictNullChecks family:
// @ts-expect-error
// @ts-no-strictNullChecks
Wait, @ts-no-strictNullChecks? Yes, you read that right. If your entire project has strictNullChecks: true in the tsconfig.json, you can use // @ts-no-strictNullChecks at the top of a particularly gnarly legacy file to temporarily disable it just for that file. This is how you stop the bleeding. You enable the strict rule globally, then whitelist the problem files until you can get to them.
The Strategy: Enable Globally, Disable Locally
Here’s the playbook. First, go into your tsconfig.json and set "strict": true or at least "strictNullChecks": true. The compiler will immediately throw a tantrum. This is fine. Now, go through the error list. Found a file that’s a complete disaster and would take a week to fix? Give it a pardon:
// @ts-no-strictNullChecks
// This file is a legacy nightmare, we'll fix it in Q2. I promise. - Dev, Jan 2023
import { SomePoorlyTypedThing } from './legacy-hell';
const result = SomePoorlyTypedThing(); // This would error under strictNullChecks, but now it won't.
This approach is brilliant because it creates a tangible, greppable list of your technical debt. Every file with that comment is a promise to your future self to come back and fix it. The goal is to make that list smaller over time, not bigger.
The Pitfall: The Illusion of Safety
Here’s the catch, and it’s a big one. The moment you start mixing strict and non-strict files, you create a boundary where all your careful type checking can fall apart. You’re building a castle with a moat, but leaving a drawbridge down for any any-typed monster to wander in.
Consider this: you have a strict file importing a function from a non-strict file.
./legacy/non-strict-file.ts (with @ts-no-strictNullChecks)
// @ts-no-strictNullChecks
export function getLegacyData() {
// This might return a string, or it might return null. Who knows? Not TypeScript!
return Math.random() > 0.5 ? "data" : null;
}
./modern/strict-file.ts (with full strict mode)
// @ts-strict
import { getLegacyData } from '../legacy/non-strict-file';
const data: string = getLegacyData(); // 💥 Type 'string | null' is not assignable to type 'string'.
// TS will now warn you about the potential null, which is GOOD!
See what happened? The non-strict file exported a function that effectively returns any (or at best, a very loose type). The strict file tries to use it, and TypeScript, now aware of null, rightly screams. This is actually the best-case scenario—it fails loudly at the boundary. The real danger is if the non-strict file’s type definitions are wrong but silently wrong, making the null slip through. You must treat these boundaries with extreme caution. Validate data at the perimeter, perhaps with a type guard.
The Best Practice: One Feature at a Time
Instead of using the sledgehammer of // @ts-strict, consider enabling the specific rules you care about most, file by file. strictNullChecks is the big one. You can also target noImplicitAny or exactOptionalPropertyTypes.
// @ts-no-strictNullChecks
// @ts-no-implicit-any
// @ts-exact-optional-property-types
This granular approach is less common but can be useful if your team wants to adopt one specific practice across the codebase before moving on to the next. The tooling is there; use it how it best suits your refactoring strategy. The goal isn’t just to make the errors go away—it’s to actually make your code more robust, one carefully considered step at a time.