TypeScript Strict Mode: What Each Flag Does and Why to Turn It On
A flag-by-flag guide to TypeScript strict mode, including noUncheckedIndexedAccess, and a practical order for enabling them on an existing codebase.
Table of contents
- What strict actually turns on
- strictNullChecks — the one that matters most
- noImplicitAny
- useUnknownInCatchVariables
- strictFunctionTypes
- strictPropertyInitialization
- The flag not included in strict — and worth adding
- Adopting strict on an existing codebase
- Frequently asked questions
- Does strict mode slow down compilation?
- Does it affect the emitted JavaScript?
- Should a new project always use strict?
- What about any in third-party types?
- Related reading
- References
"strict": true enables eight checks at once. It is the single highest-value line in a tsconfig.json, and knowing what each flag catches makes adopting it on an existing project tractable rather than terrifying.
What strict actually turns on#
{
"compilerOptions": {
"strict": true
// equivalent to enabling all of:
// strictNullChecks, strictFunctionTypes, strictBindCallApply,
// strictPropertyInitialization, strictBuiltinIteratorReturn,
// noImplicitAny, noImplicitThis, useUnknownInCatchVariables
}
}strictNullChecks — the one that matters most#
Without it, null and undefined are assignable to every type, so the compiler cannot warn you about the most common runtime error in JavaScript.
function greet(user: { name: string } | null) {
return `Hello ${user.name}`; // Error with strictNullChecks
}This flag alone catches the majority of real bugs strict mode finds.
noImplicitAny#
A parameter with no annotation and no inferable type becomes an error instead of silently becoming any.
useUnknownInCatchVariables#
try {
risky();
} catch (error) {
// error is `unknown`, not `any`
console.log(error.message); // Error — must narrow first
}Correct, and mildly annoying, which is why a messageFrom(error: unknown) helper is worth writing once.
strictFunctionTypes#
Makes function parameter types checked contravariantly, catching a subtle class of unsound assignment. You will rarely hit it deliberately.
strictPropertyInitialization#
A class property that is not undefined-able must be assigned in the constructor or marked !.
The flag not included in strict — and worth adding#
noUncheckedIndexedAccess is not part of strict, and it catches a genuinely common bug:
const items = ['a', 'b'];
const third = items[2]; // typed string — but it is undefined!
third.toUpperCase(); // crashes at runtimeWith the flag on, items[2] is string | undefined and the compiler forces a check. The cost is more ! assertions and optional chaining in code that indexes arrays in loops; the benefit is that every array access is honest about what it might return.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
}
}Those last two are cheap and catch real mistakes: a missing override keyword and a forgotten break.
Adopting strict on an existing codebase#
Turning it on wholesale in a large project produces thousands of errors and gets reverted. A sequence that works:
- Turn on
noImplicitAnyfirst. It is the easiest to fix and forces annotations that make later steps easier. - Then
strictNullChecks. This is the big one. Expect the most errors here. - Fix by directory, not by error count. Use
includein a second tsconfig to check one folder strictly while the rest stays loose. - Then the remaining flags, which are usually near-zero-cost once the first two are done.
- Finally
noUncheckedIndexedAccess.
Frequently asked questions#
Does strict mode slow down compilation?#
Negligibly. The extra analysis is a small fraction of total check time.
Does it affect the emitted JavaScript?#
No. All strict flags are compile-time only; the output is identical.
Should a new project always use strict?#
Yes, without exception. The cost of enabling it on day one is nearly zero; the cost of retrofitting it into 100,000 lines is weeks.
What about any in third-party types?#
skipLibCheck: true stops the compiler checking .d.ts files in node_modules, which is standard practice — you cannot fix those, and checking them slows builds for no benefit.
Related reading#
- Migrating a JavaScript Project to TypeScript — the wider migration path
- Optional Chaining and Nullish Coalescing — what you write once
strictNullChecksis on