Skip to content

TypeScript Configuration and Strict Mode

intermediate12 min read

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.

Mental Model

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
}
Common Trap

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"]
}
Execution Trace
Parse:
TypeScript reads tsconfig.json
Merges with extends if present, resolves paths
Strict:
strict: true enables 8 sub-flags
Each flag activates a different category of type checking
Check:
Compiler checks every file in include
Applies all enabled rules — errors on violations
Emit:
Generates .js, .d.ts, .map files
Only if no errors (or with noEmitOnError: false)
Common Mistakes
  • 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

Quiz
What does strictNullChecks actually change?
Quiz
Why is useUnknownInCatchVariables important?
Quiz
What does noUncheckedIndexedAccess change about array access?

Fix the Strict Mode Errors

Key Rules
  1. 1strict: true is non-negotiable in production — it enables 8 flags that each catch a different category of runtime error
  2. 2strictNullChecks is the most impactful flag — without it, null flows everywhere unchecked
  3. 3noUncheckedIndexedAccess should be enabled in new projects — array[n] can be out of bounds
  4. 4Never use ! (non-null assertion) as a workaround for strict mode — handle nulls properly with narrowing
  5. 5useUnknownInCatchVariables forces proper error handling because JavaScript can throw anything, not just Error instances