"strict": true in tsconfig is the floor, not the ceiling. The five extra flags below catch real bugs that pure-strict misses. This post is the working setup.

The floor

{
  "compilerOptions": {
    "strict": true,
    "target": "es2024",
    "module": "esnext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "isolatedModules": true
  }
}

Standard. What ships in tsc --init mostly.

Beyond strict

noUncheckedIndexedAccess

const xs: number[] = [1, 2, 3];
const x = xs[10];        // Pre-flag: x: number. Post-flag: x: number | undefined

Forces handling of “this index might not exist.” Catches bugs everywhere arrays / records get accessed without bounds check.

Migration pain: real. Reward: real.

exactOptionalPropertyTypes

type User = { name: string; nickname?: string };

const u: User = { name: "A", nickname: undefined };  // ⛔ with flag

Optional means “may be absent,” not “may be undefined.” Distinguishes { x?: T } from { x: T | undefined }. Catches API mismatches.

noPropertyAccessFromIndexSignature

type Headers = { [key: string]: string };
const h: Headers = {};
h.contentType;           // ⛔ with flag — must use h["Content-Type"]

Forces explicit indexing for index-signature types. Catches typos and assumptions.

noImplicitOverride

class Base {
  greet() { return "hi"; }
}

class Sub extends Base {
  greet() { return "hey"; }     // ⛔ must mark override
}

Without override, you might shadow the base method by accident. Annotation makes intent explicit.

verbatimModuleSyntax

import { Foo } from "./mod";       // ⛔ if Foo is type-only
import type { Foo } from "./mod";  // ✅

Forces you to mark type-only imports. Helps with bundlers (they can drop type-only imports cleanly).

Other useful flags

{
  "noUnusedLocals": true,
  "noUnusedParameters": true,
  "noFallthroughCasesInSwitch": true,
  "forceConsistentCasingInFileNames": true,
  "noImplicitReturns": true,
  "allowUnreachableCode": false
}

Each catches a specific class of bug or stylistic accident.

Module resolution in 2026

"module": "preserve",            // for libraries
"module": "esnext",              // for apps bundled with Vite/esbuild/Bun
"moduleResolution": "bundler",   // matches modern bundlers

"bundler" resolution matches what Vite, esbuild, Bun do. Avoids the tsc --moduleResolution node legacy behaviors.

For libraries: rougher checks

{
  "declaration": true,
  "declarationMap": true,
  "sourceMap": true,
  "stripInternal": true
}

Plus ban any returns from public API:

"noExplicitAny": "error"      // via ESLint / Biome

Migration strategy

For existing code:

  1. Enable strict first if not already. Fix.
  2. Add one extra flag at a time. Fix.
  3. Repeat.

Don’t try to enable everything at once on a 100k LoC codebase. The ergonomic tax is real; ramp.

What I’d ship today

For a new TypeScript project:

{
  "compilerOptions": {
    "target": "es2024",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,
    "isolatedModules": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  }
}

Strict ceiling. Catches the most bugs. Combined with Biome / ESLint , you have rigorous TS.

Read this next

If you want my tsconfig.json template per project shape, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .