tsconfig Strictness Flags — What Each One Prevents
TL;DR
"strict": true is a meta-flag enabling a family of stricter checks; each sub-flag prevents a specific class of bug. Beyond strict, there are additional flags (noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noFallthroughCasesInSwitch) that aren’t enabled by strict but every senior TS codebase turns on. Knowing each flag’s effect — and which class of bug it kills — is a common interview probe.
In depth
What does "strict": true actually enable?
It’s an umbrella for these (TS 5.x):
| Sub-flag | Effect |
|---|---|
noImplicitAny |
error on parameters/variables whose type can’t be inferred (no silent any) |
strictNullChecks |
null and undefined are not assignable to other types — you must handle them |
strictFunctionTypes |
function parameter types are checked contravariantly (correct math; catches assigning a more-restrictive callback than required) |
strictBindCallApply |
.bind / .call / .apply are type-checked against the function signature |
strictPropertyInitialization |
class fields must be initialized in the constructor (or marked !) |
noImplicitThis |
this of type any is an error |
alwaysStrict |
emit “use strict” and parse in strict mode |
useUnknownInCatchVariables |
catch (e) typed unknown, not any |
"strict": true is the floor for any new codebase. Disabling sub-flags individually is a smell — fix the code instead.
What’s noUncheckedIndexedAccess and why is it the most impactful “extra” flag?
With it off, arr[i] is typed T — TypeScript pretends every index hit. With it on, arr[i] is T | undefined, reflecting reality.
const xs = [1, 2, 3];
const x = xs[10];
// off: x is number — runtime is undefined, but TS thinks it's a number — bug
// on: x is number | undefined — you're forced to narrowSame for object indexed access (record[key]). Catches a huge class of off-by-one and missing-key bugs at compile time. The one flag teams ship without and regret.
What does exactOptionalPropertyTypes change?
With it off, { name?: string } accepts both omission and name: undefined. With it on, those are different — omission is fine, but name: undefined is an error unless the type is string | undefined.
type User = { name?: string };
const a: User = {}; // ok
const b: User = { name: undefined };
// off: ok
// on: error — must be { name?: string | undefined } to allow undefined explicitlyCatches a category of “I meant to delete it, but I passed undefined” bugs and aligns the type system with how JS actually distinguishes the two.
What’s noImplicitOverride?
Forces you to write override on a subclass method that overrides a base method. Prevents a class of “renamed the base method, forgot to update the subclass” bugs.
class Base { greet() { return "hi"; } }
class Sub extends Base {
// noImplicitOverride: error
greet() { return "hello"; }
override greet() { return "hello"; } // ok
}What’s noFallthroughCasesInSwitch?
Errors on a switch case that lacks a break/return/throw — the implicit fall-through that’s bitten everyone at least once.
What does useUnknownInCatchVariables change?
With it off (the old default), catch (e) types e as any — you can do anything with it without type checking. With it on, e is unknown — you must narrow:
try { /* ... */ } catch (e) {
if (e instanceof Error) console.log(e.message);
else console.log(String(e));
}Enabled by strict: true since TS 4.4. Catches “I assumed e was always an Error” bugs (JS lets you throw anything).
What’s strictPropertyInitialization?
A class field declared without an initializer or assignment in the constructor is an error.
class User {
id: number; // error: not initialized
name = "anon"; // ok — default
email!: string; // ok — definite assignment assertion (you swear it's set elsewhere)
constructor(id: number) { this.id = id; } // ok — assigned in ctor
}The ! should be rare — usually it means a framework initializes the field (Angular injection, Vue class components). Don’t sprinkle it to silence errors.
What does strictFunctionTypes change?
Function parameter types are checked contravariantly (the mathematically correct rule) instead of bivariantly. Translation: an assignment like “I want a callback (e: MouseEvent) => void” no longer accepts a callback typed (e: Event) => void even though Event is more general — you’d lose properties when called. (Note: method syntax on object types is still bivariant, even with this flag — TS historical compromise.)
What’s noImplicitReturns and noImplicitAny differ?
noImplicitAny: variables/parameters with no inferrable type.noImplicitReturns: functions where some code paths return a value and others don’t (fall off the end). Catches forgottenreturns.
Recommended starting tsconfig for a new project?
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"jsx": "react-jsx"
}
}skipLibCheck is pragmatic, not strict — it avoids slow re-checking of node_modules types; almost universally on.
Gotchas / edge cases
noUncheckedIndexedAccesscascades — once on,Object.keys(x).forEach(k => x[k])typesx[k]asT | undefined. Use a typed loop (for (const [k, v] of Object.entries(x))) instead.exactOptionalPropertyTypesbreaks third-party types that rely on{ prop?: T }acceptingundefined. Often the first flag teams want and then walk back.strictevolves — Microsoft adds sub-flags over time (useUnknownInCatchVariablesarrived in 4.4). A"strict": truecodebase upgrading TS will get new errors; that’s the trade-off.!(definite assignment assertion) silencesstrictPropertyInitialization— use sparingly; you’re claiming you’ll initialize it elsewhere.
What a senior is expected to say 4
- “
strict: trueis the floor. Beyond that,noUncheckedIndexedAccessandexactOptionalPropertyTypesare the two extras that prevent the most real bugs.” - “I treat disabling a sub-flag locally as a code smell — fix the code, don’t loosen the checker.”
- “I know
catch (e)isunknownunder strict, and I narrow withinstanceof Errorbefore readinge.message.” - “I read the
strictumbrella as a set of guarantees: no implicitany, null-safety, correct function-type variance, initialized class fields. Each protects a class of bug.”
Cross-references
unknownnarrowing in catch: Type Narrowing and Type Guards- React typing under strict (
useState<T | null>(null)): Typing React Components any/unknown/neverdistinctions: TypeScript Pitfalls — any/unknown/never, type vs interface, and Other Senior Traps
Further reading
- TS docs —
tsconfigreference: https://www.typescriptlang.org/tsconfig strictfamily: https://www.typescriptlang.org/tsconfig#strictnoUncheckedIndexedAccessrelease notes: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#checked-indexed-accesses---nouncheckedindexedaccess