I turned on TypeScript's strict mode in a two-year-old project. Here's what broke.
The project started in 2024 with "strict": false, because deadlines, and it stayed that way through two years of feature work. Flipping it on was supposed to be a quick Friday-afternoon cleanup. It took most of the week — and turned up bugs that had been sitting in production the entire time.
What strict mode actually turns on
"strict": true isn't one setting, it's shorthand for seven:
{
"strict": true
// equivalent to:
// noImplicitAny, strictNullChecks, strictFunctionTypes,
// strictBindCallApply, strictPropertyInitialization,
// noImplicitThis, alwaysStrict
}Two of those seven accounted for almost every error I saw: strictNullChecks and noImplicitAny. The other five barely registered.
strictNullChecks found three real bugs
Without it, null and undefined are assignable to everything — a string parameter happily accepts null and TypeScript says nothing. With it on, this stopped compiling:
function getDisplayName(user: User): string {
return user.nickname.toUpperCase();
}user.nickname was typed string | undefined in the database layer, and three call sites were passing users who'd never set one. In production this had been throwing Cannot read properties of undefined for months, caught by a generic error boundary that just showed "Something went wrong" — so it never showed up as a crash report anyone investigated. The type error pointed straight at the fix:
function getDisplayName(user: User): string {
return (user.nickname ?? user.email).toUpperCase();
}Two more of the same shape turned up elsewhere. Three real, silent bugs, found without writing a single test.
noImplicitAny mostly found sloppy code, not bugs
This one was noisier and lower-value. Old utility functions like this compiled fine before:
function groupBy(items, keyFn) {
// items and keyFn are both `any`
}noImplicitAny flagged every one. Fixing them was mechanical — add the parameter types — and didn't uncover any bugs, just years of accumulated untyped glue code. Necessary, but it was hours of typing signatures, not hours of finding problems.
strictNullChecks found real, currently-live bugs. noImplicitAny mostly found technical debt. If a large codebase can't take the whole strict flag at once, strictNullChecks on its own is where the return on investment is — turn it on first, separately, and see what it finds before committing to the rest.
The genuine false positive
One category of error was correct according to the type system and wrong according to reality:
const cache = new Map<string, User>();
function getUser(id: string): User {
if (!cache.has(id)) {
cache.set(id, fetchUser(id));
}
return cache.get(id); // Error: User | undefined is not assignable to User
}TypeScript can't know that the .set() call two lines above guarantees .get() will find something — it checks Map.get()'s return type in isolation, not the surrounding control flow. The fix is either a non-null assertion (cache.get(id)!) with a comment explaining why it's safe, or restructuring to avoid the two-step lookup:
function getUser(id: string): User {
const existing = cache.get(id);
if (existing) return existing;
const user = fetchUser(id);
cache.set(id, user);
return user;
}The second version is more code but doesn't need an escape hatch — worth it for anything that isn't a one-off.
Doing it incrementally
Turning on strict for the whole project at once produced 47 errors across 30 files, which is exactly the wall of errors that makes teams give up and revert. tsconfig.json supports enabling the flags one at a time:
{
"compilerOptions": {
"strictNullChecks": true
}
}Ship that alone, let it sit for a sprint, then add the next flag. Each one lands as a reviewable, bisectable change instead of one 47-error diff nobody reads carefully.
Was it worth a week
Three production bugs found before a user reported them, against a week of mostly mechanical fixes — yes, but only because strictNullChecks did the heavy lifting. If your project doesn't have two years of untyped null handling built up, don't expect the same ratio. The value is proportional to how long the codebase went without it.