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.
Interview Q&A
Q: What does "strict": true actually enable?
A: 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.
Q: What’s noUncheckedIndexedAccess and why is it the most impactful “extra” flag?
A: 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 narrow
Same 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.
Q: What does exactOptionalPropertyTypes change?
A: 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 explicitly
Catches 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.
Q: What’s noImplicitOverride?
A: 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 {
greet() { return "hello"; } // noImplicitOverride: error
override greet() { return "hello"; } // ok
}
Q: What’s noFallthroughCasesInSwitch?
A: Errors on a switch case that lacks a break/return/throw — the implicit fall-through that’s bitten everyone at least once.
Q: What does useUnknownInCatchVariables change?
A: 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).
Q: What’s strictPropertyInitialization?
A: 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.
Q: What does strictFunctionTypes change?
A: 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.)
Q: What’s noImplicitReturns and noImplicitAny differ?
A:
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.
Q: Recommended starting tsconfig for a new project?
A:
{
"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
- “
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: 05_narrowing_and_guards.md- React typing under strict (
useState<T | null>(null)): 08_react_typing.md any/unknown/neverdistinctions: 10_typescript_pitfalls.md
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