TypeScript Strict Mode: The Bugs It Catches Later
Strict mode is easy to enable and painful to retroactively satisfy. Here are the patterns that make it worth the upfront cost.
"strict": true in tsconfig.json enables eight compiler checks at once.
The first six catch bugs immediately. The last two — noImplicitAny and
strictNullChecks — catch bugs six months later, when you’d otherwise be
debugging production at 11pm.
The Two That Matter Most
noImplicitAny
Without this, TypeScript silently annotates anything it can’t infer as
any. With it, you’re forced to either annotate explicitly or accept the
annotation that the type was unknowable.
The function that bites:
function parseConfig(raw) {
// What is `raw`? string? object? Buffer? `any` by default.
return JSON.parse(raw);
}
With noImplicitAny, you must decide. The decision is the value.
strictNullChecks
Without it, null and undefined flow freely through every type. With it,
a type T does not include null unless you write T | null.
function findUser(id: string): User | undefined {
// ...
}
const user = findUser("abc");
user.name; // error: 'user' is possibly 'undefined'
The possibly undefined error feels pedantic until the day a database
query returns null and your code throws Cannot read property 'name' of undefined.
Patterns That Make Strict Bearable
Narrow with type guards, not casts.
// Bad — silences the compiler, doesn't actually check
const name = (user as User).name;
// Good — narrows the type, runs at runtime
if (user) {
const name = user.name;
}
Use the exhaustive switch for discriminated unions.
type Result = { ok: true; value: number } | { ok: false; error: string };
function handle(r: Result) {
switch (r.ok) {
case true:
return r.value;
case false:
return r.error;
default:
const _exhaustive: never = r;
return _exhaustive;
}
}
The _exhaustive: never line gives you a compile-time error if you ever
add a new variant to Result without updating handle. This is the single
best TypeScript feature, and it only works under strict mode.
Don’t reach for ! (non-null assertion).
user!.name says “I know better than the compiler.” Sometimes you do. More
often, you’ll regret it. Prefer:
if (user === undefined) throw new Error("user must exist");
user.name; // now safe
The explicit throw is debuggable. The non-null assertion isn’t.
Avoid as entirely when possible.
Type assertions are an escape hatch, not a tool. If you need to cast, ask why the type isn’t what you expect — usually the fix is upstream.
The Migration Tax
Enabling strict on an existing codebase is brutal. The strategy:
- Turn it on, accept the wall of errors.
- Pick a directory. Fix it. Commit.
- Repeat.
Don’t try to land strict mode in one PR. The cognitive load is too high and the review is meaningless.
Is It Worth It?
Every time. The bugs strict mode catches are the ones that take down production: undefined access on optional data, implicit any flowing through a hot path, a discriminated union that grew a new case nobody handled. The upfront tax is real; the late-stage payoff is larger.