Turning on strict mode without drowning in errors
What "strict" actually enables, the order to turn each flag on, and the fix patterns that clear most errors fast.
"strict": true is where TypeScript earns its keep, and where many migrations hit a wall of red. The trick is that strict isn't one switch. It's a bundle of flags you can enable one at a time.
What strict turns on
| Flag | What it catches |
|---|---|
alwaysStrict | Emits "use strict" and parses in strict mode |
noImplicitThis | this with an implicit any type |
strictBindCallApply | Wrong arguments to bind, call and apply |
strictFunctionTypes | Unsafe function parameter assignments |
noImplicitAny | Values TypeScript can't infer and silently types as any |
strictNullChecks | Using a value that might be null or undefined |
strictPropertyInitialization | Class properties never assigned in the constructor |
useUnknownInCatchVariables | Treats catch (e) as unknown instead of any |
Count before you commit
Measure each flag's cost without touching your config:
npx tsc --noEmit --noImplicitAny | grep -c "error TS"
npx tsc --noEmit --strictNullChecks | grep -c "error TS"Now you know which flags are cheap wins and which are projects.
A good order
- The cheap ones first:
alwaysStrict,noImplicitThis,strictBindCallApply,strictFunctionTypes. Usually a handful of errors. noImplicitAny. Forces real types at module boundaries, which makes the next step easier.strictNullChecks. Usually the biggest. Do it with the rest of the code already well typed.strictPropertyInitializationanduseUnknownInCatchVariablesto finish.
Then replace the individual flags with "strict": true, so future flags added to the bundle are picked up automatically.
Fix patterns that clear most errors
Narrow instead of asserting:
function total(cart: Cart | undefined) {
if (!cart) return 0;
return cart.items.reduce((sum, i) => sum + i.price * i.qty, 0);
}Optional chaining and defaults:
const city = user.address?.city ?? "Unknown";Handle unknown in catch blocks:
try {
await save();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
logger.error(message);
}Use the non-null assertion sparingly. value! tells the compiler to trust you. It's fine right after a check the compiler can't see, and a code smell anywhere else.
Escape hatches, used honestly
If you must ship before every error is fixed, prefer // @ts-expect-error over // @ts-ignore: it fails once the error is gone, so stale suppressions can't pile up. Count them and drive the number down.
grep -rn "@ts-expect-error" src | wc -lMigrate in order, again
The same rule that makes conversion work makes strict mode work: fix leaf modules first. Once lib/db.ts is strictly typed, everything that imports it gets better types for free. CodeMorph's Verify checks the whole project against your tsconfig.json, so you can ask Morph for strict mode and migrate straight to that bar.