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.
Search across all documentation pages
Enable TypeScript's strict compiler options to catch more bugs at compile time and complement ESLint with type-level safety.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
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.
// 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:
strict mode enabled as a baselinestrict for maximum safetyverbatimModuleSyntax for explicit type-only importsnoUncheckedIndexedAccess to force null checks on array and object accessnoEmit, jsx: "preserve", moduleResolution: "bundler")"strict": true is an umbrella flag that enables eight individual strict checksnoUncheckedIndexedAccess are not included in strict and must be enabled separatelytsc --noEmit runs the type checker without producing output files (Next.js handles compilation via SWC)What each strict option catches:
| Option | What It Catches |
|---|---|
strictNullChecks | Accessing possibly null or undefined values without checking |
noImplicitAny | Variables or parameters with no type that default to any |
strictFunctionTypes | Unsafe function parameter type assignments |
noUncheckedIndexedAccess | Accessing array[i] or obj[key] without null check |
noImplicitReturns | Functions that do not return a value in all code paths |
noFallthroughCasesInSwitch | Missing break or return in switch cases |
noImplicitOverride | Overriding base class methods without override keyword |
exactOptionalPropertyTypes | Assigning undefined to optional properties explicitly |
verbatimModuleSyntax | Forces 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 -ltypescript-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.
// 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 emptyThings 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).
Other ways to solve the same problem - and when each is the better choice.
| Alternative | Use When | Don't Use When |
|---|---|---|
strict: false with individual flags | Migrating a large JavaScript-to-TypeScript codebase | Starting a new project (just use strict: true) |
| ESLint type-aware rules only | You want pattern checks without changing tsconfig | You need compile-time guarantees |
// @ts-strict per-file | You want strict mode in new files but not legacy code | You can enable it project-wide |
It enables eight individual flags:
strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitializationnoImplicitAny, noImplicitThis, alwaysStrict, useUnknownInCatchVariablesstrictNullChecks -- it has the highest impact and catches the most real bugs.noImplicitAny second.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.
array[i] and obj[key] becomes T | undefined.items[0]! sparingly for verified values, or use .at(0) with a check.import type for type-only imports.npx tsc --noEmit 2>&1 | grep "error TS" | wc -lTrack this number over time to measure migration progress.
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).undefined explicitly to optional properties.no-floating-promises and no-misused-promises require full type information.tsc --noEmit only type-checks without producing output files.// @ts-expect-error -- known safe, will fix in JIRA-123
const value = riskyFunction();@ts-expect-error temporarily for known-safe code during incremental migration.@ts-ignore, it errors if the suppressed issue is fixed (keeping suppressions honest).{
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.
tsc --noEmit in CIReviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥