frontend / typescript / 09_strictness_flags.md

tsconfig Strictness Flags — What Each One Prevents

5 min read source

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 forgotten returns.

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

  • noUncheckedIndexedAccess cascades — once on, Object.keys(x).forEach(k => x[k]) types x[k] as T | undefined. Use a typed loop (for (const [k, v] of Object.entries(x))) instead.
  • exactOptionalPropertyTypes breaks third-party types that rely on { prop?: T } accepting undefined. Often the first flag teams want and then walk back.
  • strict evolves — Microsoft adds sub-flags over time (useUnknownInCatchVariables arrived in 4.4). A "strict": true codebase upgrading TS will get new errors; that’s the trade-off.
  • ! (definite assignment assertion) silences strictPropertyInitialization — use sparingly; you’re claiming you’ll initialize it elsewhere.

What a senior is expected to say

  • strict: true is the floor. Beyond that, noUncheckedIndexedAccess and exactOptionalPropertyTypes are 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) is unknown under strict, and I narrow with instanceof Error before reading e.message.”
  • “I read the strict umbrella as a set of guarantees: no implicit any, null-safety, correct function-type variance, initialized class fields. Each protects a class of bug.”

Cross-references

Further reading