ESC

Type to search the knowledge base.

tsconfig Strict Flags

What strict mode actually enables, flag-by-flag impact on frontend code, and a practical migration order for legacy apps.

intermediate3 min read
  • typescript
  • tsconfig-strict

"strict": true is the baseline for serious TypeScript. It is a bundle of checks. Knowing the pieces helps you migrate legacy apps one lever at a time and explain tradeoffs in reviews.

Docs: TSConfig Reference — strict, Compiler Options.

What strict turns on

As of modern TypeScript, strict includes roughly:

Flag Effect
noImplicitAny Error on implied any
strictNullChecks null/undefined not in every type
strictFunctionTypes Stronger function param checking (contravariance)
strictBindCallApply Typed bind/call/apply
strictPropertyInitialization Class props must be initialized
noImplicitThis this must be typed
alwaysStrict Emit/parse in JS strict mode
useUnknownInCatchVariables catch (e) → unknown (in current strict bundles / recommended)

Exact membership can evolve — check your version’s docs. Enabling strict is still the right default for new apps.

The two that change frontend code most

strictNullChecks

const el = document.getElementById('app');
// el is HTMLElement | null
// el.textContent = 'x'; // error
el?.textContent = 'x'; // wrong assignment form
if (el) el.textContent = 'x';

Optional chaining and nullish coalescing become daily tools.

noImplicitAny

// error under noImplicitAny
function log(x) {
  console.log(x);
}

function log(x: unknown) {
  console.log(x);
}
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "skipLibCheck": true
  }
}
Flag Why
noUncheckedIndexedAccess arr[i] and obj[key] include undefined
noImplicitOverride Must mark override on subclasses
exactOptionalPropertyTypes Distinguishes missing vs undefined
skipLibCheck Faster builds; skip typechecking .d.ts noise

noUncheckedIndexedAccess is noisy but catches real bugs. Enable when the team can pay the annotation cost.

Minimal modern app skeleton

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "jsx": "react-jsx",
    "strict": true,
    "noEmit": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] }
  },
  "include": ["src"]
}

Bundlers own emit; noEmit + isolatedModules matches Vite/Next patterns. See path aliases.

Migrating a loose codebase

  1. Enable strict on new packages only (project references).
  2. Or enable flag-by-flag: noImplicitAny → strictNullChecks → rest.
  3. Use // @ts-expect-error with tickets — not blanket any.
  4. Don’t disable strict in CI “temporarily” without an owner.
// Prefer
// @ts-expect-error legacy API — JIRA-123
legacyCall();

// Avoid
// @ts-ignore

Footguns

  1. strict: false with some flags true — document the matrix.
  2. Different tsconfigs for app vs tests — tests often need DOM + vitest types.
  3. skipLibCheck: false on huge monorepos — slow CI.
  4. Assuming strict fixes runtime — still validate network data.

Interview out-loud answer

“strict enables a set of soundness checks; null checks and noImplicitAny are the big frontend ones. I start new apps with strict: true, consider noUncheckedIndexedAccess, and migrate legacy code flag-by-flag with ts-expect-error rather than any sprawl.”

CI command

"scripts": {
  "typecheck": "tsc -p tsconfig.json --noEmit"
}

Run typecheck in CI on every PR. Locally, editor typechecking is not enough if skipLibCheck or project references hide another package’s errors — typecheck the package graph you ship.

Further reading

Related guides