TypeScript Configuration and Strict Mode
The Config File That Controls Everything
Every TypeScript project has a tsconfig.json that controls how the compiler behaves. And there's one decision that matters more than all the others combined: whether strict: true is enabled. In production codebases, there is no valid reason to have it off. Every flag under strict catches real bugs that would otherwise reach production. No exceptions.
Think of strict: true as a security system with multiple sensors. Each strict flag is a different sensor: one detects null access, one detects implicit any, one detects unchecked function calls. With strict: true, all sensors are on. You can turn individual sensors off, but the system works best with everything armed. Disabling a sensor doesn't remove the threat — it just means you won't be warned about it.
What strict: true Enables
So what does it actually do? strict: true is a shorthand that enables all of these flags:
{
"compilerOptions": {
"strict": true
// Equivalent to enabling ALL of:
// "noImplicitAny": true,
// "strictNullChecks": true,
// "strictFunctionTypes": true,
// "strictBindCallApply": true,
// "strictPropertyInitialization": true,
// "noImplicitThis": true,
// "useUnknownInCatchVariables": true,
// "alwaysStrict": true
}
}
Let's examine each one and the bugs it catches.
noImplicitAny — No Silent any
// WITHOUT noImplicitAny — parameter is silently 'any'
function greet(name) {
return `Hello, ${name.toUpperCase()}`;
}
greet(42); // No error — 42 is any, any.toUpperCase() is any. Runtime crash.
// WITH noImplicitAny — forces you to annotate
function greet(name: string) { // Must declare type
return `Hello, ${name.toUpperCase()}`;
}
greet(42); // ERROR: Argument of type 'number' is not assignable to 'string'
strictNullChecks — null Is Not Assignable to Everything
// WITHOUT strictNullChecks — null flows everywhere silently
const user: User = getUser(id); // Could return null — no warning
console.log(user.name); // Runtime crash if null
// WITH strictNullChecks — null must be handled explicitly
const user: User | null = getUser(id);
console.log(user.name); // ERROR: Object is possibly 'null'
if (user) {
console.log(user.name); // OK — narrowed
}
strictNullChecks is the single most impactful flag. Without it, null and undefined are assignable to every type, making the entire type system unreliable. A codebase without strictNullChecks essentially has no null safety — the #1 source of runtime errors. There is no legitimate reason to disable this flag.
strictFunctionTypes — Correct Function Variance
// WITHOUT strictFunctionTypes — unsound function assignments
type Handler = (event: MouseEvent) => void;
const handler: Handler = (event: Event) => { // Allowed (wrong!)
console.log(event.clientX); // Runtime crash — Event doesn't have clientX
};
// WITH strictFunctionTypes — function parameter types checked contravariantly
const handler: Handler = (event: Event) => {}; // ERROR
// Parameter type 'Event' is not assignable to 'MouseEvent'
strictBindCallApply — Type-Safe bind/call/apply
function greet(name: string, age: number) {
return `${name} is ${age}`;
}
// WITHOUT strictBindCallApply
greet.call(null, "Alice", "thirty"); // No error — "thirty" is not checked
// WITH strictBindCallApply
greet.call(null, "Alice", "thirty"); // ERROR: Argument of type 'string' is not assignable to 'number'
strictPropertyInitialization — Class Properties Must Be Initialized
class User {
name: string; // ERROR: Property 'name' has no initializer
age: number; // ERROR: same
constructor() {
// Forgot to initialize name and age
}
}
// Fixed
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
// Or with the definite assignment assertion (when initialized elsewhere)
class User {
name!: string; // ! means "I know this will be initialized"
// Use sparingly — it's a cast that bypasses the check
}
useUnknownInCatchVariables — Catch Variables Are unknown
// WITHOUT useUnknownInCatchVariables
try {
riskyOperation();
} catch (error) {
// error: any — can do anything with it
console.log(error.message); // No error, but might crash if error isn't an Error
}
// WITH useUnknownInCatchVariables
try {
riskyOperation();
} catch (error) {
// error: unknown — must narrow before using
if (error instanceof Error) {
console.log(error.message); // OK — narrowed
} else {
console.log("Unknown error:", String(error));
}
}
Beyond Strict: Essential Additional Flags
Here's the thing — strict: true is the baseline, not the finish line. These additional flags catch even more bugs:
{
"compilerOptions": {
"strict": true,
// Additional type safety
"noUncheckedIndexedAccess": true, // array[0] returns T | undefined
"exactOptionalPropertyTypes": true, // {x?: number} means x can be missing but NOT explicitly undefined
"noPropertyAccessFromIndexSignature": true, // forces bracket notation for index signatures
// Code quality
"noUnusedLocals": true, // Error on unused variables
"noUnusedParameters": true, // Error on unused function parameters
"noImplicitReturns": true, // Every code path must return
"noFallthroughCasesInSwitch": true, // Switch cases must break or return
// Module resolution
"moduleResolution": "bundler", // For modern bundler-based projects
"verbatimModuleSyntax": true, // import type must use 'type' keyword
// Output
"target": "ES2022", // Modern target — avoid unnecessary downleveling
"module": "ESNext", // ESM output
"declaration": true, // Generate .d.ts files (for libraries)
"sourceMap": true // Source maps for debugging
}
}
noUncheckedIndexedAccess — the underrated flag
Without this flag, array access and index signature access always return the value type without undefined:
const arr = [1, 2, 3];
const x = arr[10]; // Without flag: number. With flag: number | undefined.
const map: Record<string, User> = {};
const user = map["nonexistent"]; // Without flag: User. With flag: User | undefined.This flag prevents one of the most common runtime errors — accessing an index that doesn't exist. The cost is slightly more verbose code (you need null checks after array access), but it catches real bugs. Most production codebases should enable this.
Production tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
-
Wrong: Leaving strict: false to avoid 'too many errors' Right: Enable strict: true and fix errors incrementally — every error is a real bug waiting to happen
-
Wrong: Using ! (non-null assertion) everywhere to silence strict null checks Right: Handle nulls properly with if checks, optional chaining, or nullish coalescing
-
Wrong: Disabling strictFunctionTypes for callback compatibility Right: Fix the callback types — use proper function variance
-
Wrong: Not enabling noUncheckedIndexedAccess in new projects Right: Enable it — array[n] and record['key'] should return T | undefined
Fix the Strict Mode Errors
- 1strict: true is non-negotiable in production — it enables 8 flags that each catch a different category of runtime error
- 2strictNullChecks is the most impactful flag — without it, null flows everywhere unchecked
- 3noUncheckedIndexedAccess should be enabled in new projects — array[n] can be out of bounds
- 4Never use ! (non-null assertion) as a workaround for strict mode — handle nulls properly with narrowing
- 5useUnknownInCatchVariables forces proper error handling because JavaScript can throw anything, not just Error instances