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
strictmode enabled as a baseline - Additional checks beyond
strictfor maximum safety verbatimModuleSyntaxfor explicit type-only importsnoUncheckedIndexedAccessto force null checks on array and object access- Next.js-compatible settings (
noEmit,jsx: "preserve",moduleResolution: "bundler")
Deep Dive
How It Works
"strict": trueis an umbrella flag that enables eight individual strict checks- Additional flags like
noUncheckedIndexedAccessare not included instrictand 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 --noEmitruns the type checker without producing output files (Next.js handles compilation via SWC)
Variations
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.
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 emptyGotchas
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-errortemporarily for known-safe code and fix errors file by file. -
noUncheckedIndexedAccess is noisy — Every
array[i]andobj[key]becomesT | 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 assignundefinedto 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-promisesrequire 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.
| 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 |
FAQs
What does "strict": true enable?
It enables eight individual flags:
strictNullChecks,strictFunctionTypes,strictBindCallApply,strictPropertyInitializationnoImplicitAny,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
noImplicitAnysecond. - 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]andobj[key]becomesT | 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 typefor 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 -lTrack this number over time to measure migration progress.
What is the difference between TypeScript strict options and ESLint TypeScript rules?
tsconfigstrict options catch type-level issues (null safety, implicit any, type mismatches).@typescript-eslintrules 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
undefinedexplicitly 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-promisesandno-misused-promisesrequire 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 --noEmitonly 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-errortemporarily 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.
Related
- Essential ESLint Rules — ESLint rules that complement strict TypeScript
- ESLint Plugins — @typescript-eslint plugin configuration
- Linting in CI/CD — running
tsc --noEmitin CI - ESLint Setup for Next.js — base ESLint configuration