React SME Cookbook
All FAQs

Search Documentation

Search across all documentation pages

typescriptstricttsconfigcompilertype-safety

TypeScript Strict Mode & Compiler Checks

Enable TypeScript's strict compiler options to catch more bugs at compile time and complement ESLint with type-level safety.

Recipe

Quick-reference recipe card — copy-paste ready.

# Check types without emitting files
npx tsc --noEmit
 
# Check types in watch mode during development
npx tsc --noEmit --watch
 
# Add to package.json
# "type-check": "tsc --noEmit"

When to reach for this: Every TypeScript project should enable strict mode. The question is not whether, but how quickly you can turn on each option.

Working Example

// tsconfig.json
{
  "compilerOptions": {
    // --- Strict mode (umbrella flag) ---
    "strict": true,
    // "strict" enables all of these:
    //   "strictNullChecks": true,
    //   "strictFunctionTypes": true,
    //   "strictBindCallApply": true,
    //   "strictPropertyInitialization": true,
    //   "noImplicitAny": true,
    //   "noImplicitThis": true,
    //   "alwaysStrict": true,
    //   "useUnknownInCatchVariables": true,
 
    // --- Additional strict checks (not in "strict") ---
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,
 
    // --- Module and import settings ---
    "verbatimModuleSyntax": true,
    "moduleResolution": "bundler",
    "module": "esnext",
    "target": "es2017",
 
    // --- Path aliases ---
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    },
 
    // --- Output ---
    "noEmit": true,
    "jsx": "preserve",
    "incremental": true,
 
    // --- Interop ---
    "esModuleInterop": true,
    "allowJs": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "skipLibCheck": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

What this demonstrates:

  • Full strict mode enabled as a baseline
  • Additional checks beyond strict for maximum safety
  • verbatimModuleSyntax for explicit type-only imports
  • noUncheckedIndexedAccess to force null checks on array and object access
  • Next.js-compatible settings (noEmit, jsx: "preserve", moduleResolution: "bundler")

Deep Dive

How It Works

  • "strict": true is an umbrella flag that enables eight individual strict checks
  • Additional flags like noUncheckedIndexedAccess are not included in strict and must be enabled separately
  • TypeScript checks run at compile time and catch an entire class of bugs that ESLint cannot (type mismatches, missing properties, incorrect function signatures)
  • tsc --noEmit runs the type checker without producing output files (Next.js handles compilation via SWC)

Variations

What each strict option catches:

OptionWhat It Catches
strictNullChecksAccessing possibly null or undefined values without checking
noImplicitAnyVariables or parameters with no type that default to any
strictFunctionTypesUnsafe function parameter type assignments
noUncheckedIndexedAccessAccessing array[i] or obj[key] without null check
noImplicitReturnsFunctions that do not return a value in all code paths
noFallthroughCasesInSwitchMissing break or return in switch cases
noImplicitOverrideOverriding base class methods without override keyword
exactOptionalPropertyTypesAssigning undefined to optional properties explicitly
verbatimModuleSyntaxForces import type for type-only imports

Enabling strict mode incrementally:

// Step 1: Start with strict false but enable options one by one
{
  "compilerOptions": {
    "strict": false,
    "strictNullChecks": true,    // Enable first — highest impact
    "noImplicitAny": true         // Enable second
    // Add more as you fix errors
  }
}
# Count remaining errors to track progress
npx tsc --noEmit 2>&1 | grep "error TS" | wc -l

typescript-eslint rules that complement tsconfig:

// These ESLint rules catch patterns tsc does not flag:
{
  rules: {
    "@typescript-eslint/no-floating-promises": "error",    // Unhandled promises
    "@typescript-eslint/no-misused-promises": "error",     // Promises in wrong contexts
    "@typescript-eslint/await-thenable": "error",           // await on non-Promise
    "@typescript-eslint/no-unnecessary-condition": "warn",  // Always-true conditions
    "@typescript-eslint/prefer-nullish-coalescing": "warn", // || vs ??
    "@typescript-eslint/strict-boolean-expressions": "warn", // Truthy checks on non-booleans
  },
}

Note: These rules require parserOptions.project to be set for type-aware linting.

TypeScript Notes

// verbatimModuleSyntax enforces explicit type imports:
import type { User } from "@/types";        // Type-only — erased at runtime
import { fetchUser } from "@/lib/api";       // Value — kept at runtime
 
// noUncheckedIndexedAccess in action:
const items = ["a", "b", "c"];
const first = items[0]; // Type: string | undefined (not string)
if (first) {
  console.log(first.toUpperCase()); // Safe — narrowed to string
}
 
// Without noUncheckedIndexedAccess:
const risky = items[0]; // Type: string (lies — could be undefined)
risky.toUpperCase(); // Runtime crash if array is empty

Gotchas

Things that will bite you. Each gotcha includes what goes wrong, why it happens, and the fix.

  • strictNullChecks creates many errors — Enabling this on a large existing project can produce hundreds or thousands of errors. Fix: Enable incrementally. Use // @ts-expect-error temporarily for known-safe code and fix errors file by file.

  • noUncheckedIndexedAccess is noisy — Every array[i] and obj[key] becomes T | undefined, requiring null checks even when you know the value exists. Fix: Use non-null assertion (items[0]!) sparingly where you have verified the value exists, or use .at(0) with a null check.

  • exactOptionalPropertyTypes breaks common patterns — You can no longer write { name?: string } and assign undefined to it explicitly. Fix: Only enable this if your codebase distinguishes between "missing" and "explicitly undefined" properties. Most projects skip this option.

  • Type-aware ESLint rules are slow — Rules like no-floating-promises require full type information, making ESLint 2 to 5 times slower. Fix: Only enable type-aware rules if the trade-off is worth it. Consider running them only in CI, not in the editor.

  • verbatimModuleSyntax and CommonJS — This option does not work well with CommonJS modules. Fix: Only enable it in projects using ES modules (which includes all Next.js App Router projects).

Alternatives

Other ways to solve the same problem — and when each is the better choice.

AlternativeUse WhenDon't Use When
strict: false with individual flagsMigrating a large JavaScript-to-TypeScript codebaseStarting a new project (just use strict: true)
ESLint type-aware rules onlyYou want pattern checks without changing tsconfigYou need compile-time guarantees
// @ts-strict per-fileYou want strict mode in new files but not legacy codeYou can enable it project-wide

FAQs

What does "strict": true enable?

It enables eight individual flags:

  • strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization
  • noImplicitAny, noImplicitThis, alwaysStrict, useUnknownInCatchVariables
Which strict option should I enable first on a legacy codebase?
  • Start with strictNullChecks -- it has the highest impact and catches the most real bugs.
  • Enable noImplicitAny second.
  • Add more options one by one as you fix errors.
What does noUncheckedIndexedAccess do?
const items = ["a", "b", "c"];
const first = items[0]; // Type: string | undefined (not string)
if (first) {
  console.log(first.toUpperCase()); // Safe after narrowing
}

It forces null checks on every array and object index access.

Gotcha: Why does noUncheckedIndexedAccess produce so many errors?
  • Every array[i] and obj[key] becomes T | undefined.
  • You must add null checks even when you know the value exists.
  • Use items[0]! sparingly for verified values, or use .at(0) with a check.
What is verbatimModuleSyntax and when should I use it?
  • It forces explicit import type for type-only imports.
  • Type imports are erased at runtime; value imports are kept.
  • Only enable in ES module projects (all Next.js App Router projects qualify).
How do I count remaining type errors when migrating?
npx tsc --noEmit 2>&1 | grep "error TS" | wc -l

Track this number over time to measure migration progress.

What is the difference between TypeScript strict options and ESLint TypeScript rules?
  • tsconfig strict options catch type-level issues (null safety, implicit any, type mismatches).
  • @typescript-eslint rules catch code patterns (floating promises, unnecessary conditions, explicit any usage).
  • Use both for maximum coverage.
Gotcha: Should I enable exactOptionalPropertyTypes?
  • Most projects skip this option.
  • It prevents assigning undefined explicitly to optional properties.
  • Only enable it if your codebase distinguishes between "missing" and "explicitly undefined."
What are type-aware ESLint rules and why are they slow?
  • Rules like no-floating-promises and no-misused-promises require full type information.
  • ESLint must invoke the TypeScript compiler, making it 2-5x slower.
  • Consider running type-aware rules only in CI, not in the editor.
Why does Next.js use noEmit: true in tsconfig?
  • Next.js handles compilation via SWC, not the TypeScript compiler.
  • tsc --noEmit only type-checks without producing output files.
  • This separates type checking from bundling.
How do TypeScript strict options work with @ts-expect-error?
// @ts-expect-error -- known safe, will fix in JIRA-123
const value = riskyFunction();
  • Use @ts-expect-error temporarily for known-safe code during incremental migration.
  • It suppresses the error on the next line only.
  • Unlike @ts-ignore, it errors if the suppressed issue is fixed (keeping suppressions honest).
What ESLint rules complement tsconfig strict options?
{
  rules: {
    "@typescript-eslint/no-floating-promises": "error",
    "@typescript-eslint/no-misused-promises": "error",
    "@typescript-eslint/await-thenable": "error",
    "@typescript-eslint/prefer-nullish-coalescing": "warn",
  }
}

These catch patterns that tsc does not flag.