TypeScript strict Mode — What Each Flag Actually Catches
TL;DR: strict turns on eight separate checks. Each catches a different category of bug. Knowing which is which makes you better at picking which to enable mid-migration.
strict is eight flags in a trench coat
Setting "strict": true in tsconfig.json enables eight separate strictness flags. You can enable them individually for incremental migration.
{
"compilerOptions": {
"strict": true
// equivalent to:
// "noImplicitAny": true,
// "strictNullChecks": true,
// "strictFunctionTypes": true,
// "strictBindCallApply": true,
// "strictPropertyInitialization": true,
// "noImplicitThis": true,
// "useUnknownInCatchVariables": true,
// "alwaysStrict": true
}
}
noImplicitAny — catches forgotten annotations
Without it, every parameter without an annotation silently becomes any. With it, you have to annotate or explicitly say : any — which makes review easy.
strictNullChecks — the big one
Forces null/undefined to flow through your types instead of hiding everywhere. Most of the bugs strict catches come from this single flag.
// without strictNullChecks
function greet(name: string) { return "hi " + name; }
greet(null); // compiles, runtime crashes
// with strictNullChecks
function greet(name: string) { return "hi " + name; }
greet(null); // type error
strictFunctionTypes — variance correctness
Makes function parameters checked contravariantly (correctly), not bivariantly. Catches a class of subtype-substitution bugs.
strictPropertyInitialization
Class fields must be assigned in the constructor or have a definite assignment hint (!:). Catches the 'I forgot to initialize' bug.
class User {
name: string; // error — not initialized
age!: number; // ok — definite assignment hint, you promise
email = ""; // ok — has default
constructor(name: string) {
this.name = name;
}
}
noImplicitThis
Catches this referring to any inside callbacks. Rare in modern code (arrow functions sidestep it), still useful as a guardrail.
useUnknownInCatchVariables
catch (e) defaults to unknown instead of any. Forces you to narrow before using the error — which catches 'oh I assumed it was Error' bugs.
try {
await fetchUser();
} catch (e) {
if (e instanceof Error) {
log.error(e.message);
} else {
log.error("unknown error", e);
}
}
Migration order for legacy codebases
If you're enabling strict in chunks: noImplicitAny first (mechanical fix), then strictNullChecks (the painful one), then the rest in any order. strictPropertyInitialization last — it usually requires class refactoring.