Technology · TypeScript

TypeScript Strict Mode

Understand strict mode benefits, gotchas, and how to enable it progressively.

TL;DR
  1. 01Enable "strict": true in tsconfig.json for maximum type safety.
  2. 02Strict mode includes multiple flags that can be enabled individually.
  3. 03Migrate existing code gradually by enabling flags one at a time.

Enabling Strict Mode

  • Enable strict mode to enable all strict flags at once.
    {
      "compilerOptions": {
        "strict": true
      }
    }
  • Strict mode enables these flags simultaneously.
    {
      "compilerOptions": {
        "noImplicitAny": true,
        "noImplicitThis": true,
        "strictNullChecks": true,
        "strictFunctionTypes": true,
        "strictBindCallApply": true,
        "strictPropertyInitialization": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true
      }
    }
  • New TypeScript projects should use strict mode from the start.
    {
      "compilerOptions": {
        "strict": true,
        "target": "ES2020",
        "module": "ESNext"
      }
    }
  • Existing projects can migrate gradually by enabling individual flags.
    {
      "compilerOptions": {
        "strict": false,
        "noImplicitAny": true
      }
    }
  • Use noEmitOnError to prevent emitting code when type errors exist.
    {
      "compilerOptions": {
        "strict": true,
        "noEmitOnError": true
      }
    }

Implicit Any Errors

  • Require types for variables without explicit type annotations.
    // With noImplicitAny: error
    function add(a, b) {
      return a + b;
    }
    
    // Fixed: explicit types
    function add(a: number, b: number): number {
      return a + b;
    }
  • Enable noImplicitAny to catch typing mistakes early.
    // Error: parameter 'event' implicitly has an 'any' type
    document.addEventListener("click", (event) => { });
    
    // Fixed
    document.addEventListener("click", (event: MouseEvent) => { });
  • Annotate callback parameters that TypeScript cannot infer.
    // Error: 'item' implicitly has type 'any'
    const items = JSON.parse(data);
    items.forEach((item) => console.log(item));
    
    // Fixed: type the parsed value
    const items: string[] = JSON.parse(data);
    items.forEach((item: string) => console.log(item));
  • Use unknown instead of any when the type is genuinely unknown.
    function process(input: unknown) {
      if (typeof input === "string") {
        console.log(input.toUpperCase()); // Safe after narrowing
      }
    }
  • Reduces bugs from unintended type mismatches throughout the codebase.
    // Without noImplicitAny: silent runtime bug
    function double(n) { return n * 2; }
    double("3"); // "33" at runtime — no compile-time error

Null Check Errors

  • Treat null and undefined as distinct types.
    // With strictNullChecks: error
    const name: string = null;
    
    // Fixed: include null in type
    const name: string | null = null;
  • Require null checks before accessing properties.
    function getName(user: User | null) {
      if (!user) return "Guest";
      return user.name; // Safe: user is not null here
    }
  • Prevents many common runtime errors from unchecked nulls.
    // Error: Object is possibly undefined
    const first = items[0].name;
    
    // Fixed
    const first = items[0]?.name ?? "Unknown";
  • Enable strictNullChecks independently before enabling full strict mode.
    {
      "compilerOptions": {
        "strictNullChecks": true
      }
    }
  • Most impactful strict flag — catching nullable bugs early saves debugging time.
    // Without strictNullChecks: no error, crashes at runtime
    const user = getUser(); // may return null
    console.log(user.name); // TypeError: Cannot read properties of null

Property Init Errors

  • Require class properties to be initialized in the constructor.
    // With strictPropertyInitialization: error
    class User {
      name: string;
    }
    
    // Fixed: initialize in constructor
    class User {
      name: string;
      constructor(name: string) {
        this.name = name;
      }
    }
  • Use definite assignment assertion (!) if initialization is deferred.
    class User {
      name!: string; // tells TypeScript: trust me, this will be set
      
      async init() {
        this.name = await fetchName();
      }
    }
  • Initialize properties with a default value to satisfy the compiler.
    class Config {
      retries: number = 3;
      timeout: number = 5000;
      debug: boolean = false;
    }
  • Use readonly with constructor initialization for immutable properties.
    class Product {
      readonly id: string;
      readonly name: string;
      
      constructor(id: string, name: string) {
        this.id = id;
        this.name = name;
      }
    }
  • Mark properties that are set outside the constructor with declare if needed.
    class Widget {
      declare element: HTMLElement; // set by framework before use
      
      render() {
        this.element.innerHTML = "Hello";
      }
    }

Progressive Migration

  • Disable strict mode initially for large migrations.
    {
      "compilerOptions": {
        "strict": false
      }
    }
  • Enable individual flags gradually as code is typed.
    {
      "compilerOptions": {
        "strict": false,
        "noImplicitAny": true
      }
    }
  • Use stricter settings for new code while migrating old code.
    {
      "compilerOptions": {
        "strict": false
      },
      "ts-node": {
        "compilerOptions": {
          "strict": true
        }
      }
    }
  • Gradually increase strictness until full strict mode is enabled.
    {
      "compilerOptions": {
        "noImplicitAny": true,
        "strictNullChecks": true,
        "strictPropertyInitialization": true,
        "strict": true
      }
    }
  • Use @ts-strict-ignore comments to suppress errors in files not yet migrated.
    // @ts-strict-ignore
    // This file is pending strict migration — tracked in issue #42
    export function legacyHelper(data) {
      return data.value;
    }

Tip: Enable strict mode from the start on new projects — fixing types upfront is easier than retrofitting them later.

Warning: Enabling strict mode on large existing codebases requires significant effort — plan for gradual migration over multiple sprints.

TypeScript Performance TipsTypeScript With React Patterns