tsconfig Strictness Flags — What Each One Prevents
"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.
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
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.
Related
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
Interview angle 4
- “What does
strict: trueactually give you?” - a set of guarantees rather than a vibe: no implicitany, null and undefined are tracked, function types are checked contravariantly on parameters, and class fields must be initialised. Each one closes a distinct class of bug, which is why it is the floor rather than an option. - “Which flags would you add beyond
strict?” -noUncheckedIndexedAccessandexactOptionalPropertyTypes. The first makesarr[i]andrecord[key]returnT | undefined, which is the truth and catches an enormous number of real crashes. The second stops{ a?: string }silently accepting an explicitundefined.- Follow-up: “Why is
noUncheckedIndexedAccessnot instrict?” - because it is noisy on existing code. Every index access needs a check or a non-null assertion, so it is a deliberate opt-in rather than something to turn on across a large codebase in one commit.
- Follow-up: “Why is
- “What is
catch (e)typed as under strict?” -unknown, viauseUnknownInCatchVariables. You cannot reade.messagewithout narrowing, andinstanceof Erroris the narrowing - which is correct, because anything can be thrown. - “Someone disabled a flag for one file. Reaction?” - treat it as a code smell and look at the code, not the config. Loosening the checker to make an error go away removes the signal and keeps the defect, and the comment explaining why is almost never written.