TypeScriptStrict Mode

Strict Mode

TypeScript's strict mode is a single tsconfig flag that enables a family of compiler checks designed to catch the most common and dangerous runtime errors at compile time. Enabling "strict": true is the single most impactful decision you can make in a TypeScript project.

This page explains what strict mode is, what it enables, why each check matters, and how to migrate an existing codebase to it incrementally.

What "strict": true Actually Does

The strict flag is a shorthand that turns on a bundle of individual strict-family flags simultaneously. Think of it as an alias:

JSON
// "strict": true is equivalent to setting all of these:
{
  "compilerOptions": {
    "strictNullChecks":           true,
    "strictFunctionTypes":        true,
    "strictBindCallApply":        true,
    "strictPropertyInitialization": true,
    "noImplicitAny":              true,
    "noImplicitThis":             true,
    "useUnknownInCatchVariables": true,
    "alwaysStrict":               true
  }
}
Note
New strict-family flags added to TypeScript in future versions are NOT automatically enabled by an existing "strict": true setting — you must upgrade TypeScript and they opt in only at the release. This is a deliberate design choice to avoid surprise breakage on upgrades.
Why Enable Strict Mode?

Without strict mode, TypeScript is surprisingly permissive. Whole categories of bugs that TypeScript was designed to prevent are silently allowed:

TS
// --- WITHOUT strict mode ---

function getUsername(user) {          // 'user' implicitly any — no error
  return user.profile.name;           // crashes at runtime if user is null
}

class Service {
  private url: string;                // not initialized — no error!
  // constructor omitted

  fetch() {
    return fetch(this.url);           // this.url is undefined at runtime
  }
}

getUsername(null);                    // no error, but crashes at runtime

TS
// --- WITH strict mode ---

// ❌ Error: Parameter 'user' implicitly has an 'any' type.
function getUsername(user) { ... }

// ✅ Fix: explicit type with null handling
interface User { profile: { name: string } | null }

function getUsername(user: User | null): string {
  if (!user || !user.profile) return 'Anonymous';
  return user.profile.name;
}

// ❌ Error: Property 'url' has no initializer and is not definitely assigned.
class Service {
  private url: string;   // must initialize or mark as 'url!: string'
}
strictNullChecks

This is arguably the most valuable check in the entire set. Without it, null and undefined are assignable to every type, silently escaping into your logic.

With strictNullChecks: true, null and undefined are their own distinct types and must be handled explicitly.

TS
// Without strictNullChecks — these all compile silently
const x: string = null;             // allowed!
const y: number = undefined;        // allowed!

function greet(name: string) {
  console.log(name.toUpperCase());  // crashes if null is passed
}
greet(null); // no error!

TS
// With strictNullChecks — the compiler forces you to handle nullability

// ❌ Error: Type 'null' is not assignable to type 'string'.
const x: string = null;

// ✅ Use a union type to explicitly allow null
const x: string | null = null;

// DOM APIs return null — you must handle it
const button = document.getElementById('submit');
// button is: HTMLElement | null

// ❌ Error: Object is possibly null
button.addEventListener('click', handler);

// ✅ Options:
button?.addEventListener('click', handler);  // optional chaining
if (button) button.addEventListener('click', handler);
button!.addEventListener('click', handler);  // non-null assertion (use sparingly)
Tip
strictNullChecks on its own (without full strict mode) is a common first step when migrating a large JavaScript codebase to TypeScript.
noImplicitAny

TypeScript infers types when possible, but when it cannot infer a type for a parameter it falls back to any. noImplicitAny turns these silent any inferences into errors.

TS
// Without noImplicitAny
function transform(data) {     // data: any — silently passes through unchecked
  return data.map(x => x * 2); // crashes at runtime if data is not an array
}

// With noImplicitAny
// ❌ Error: Parameter 'data' implicitly has an 'any' type.
function transform(data) { ... }

// ✅ Fix: annotate the parameter
function transform(data: number[]): number[] {
  return data.map(x => x * 2);
}

// ✅ Also acceptable when intent is genuinely unknown:
function transform(data: unknown): unknown { ... }

// Object destructuring also triggers it:
// ❌ Error: Binding element 'id' implicitly has an 'any' type.
function processUser({ id, name }) { ... }

// ✅ Fix:
function processUser({ id, name }: { id: number; name: string }) { ... }
strictFunctionTypes

Without this flag, TypeScript uses bivariant checking for function types — a parameter type is checked both as a subtype and a supertype. This allows subtle unsound assignments that can cause runtime errors.

With strictFunctionTypes: true, function parameters are checked contravariantly, which is the mathematically correct approach.

TS
interface Animal { name: string }
interface Dog extends Animal { bark(): void }

// A function that handles any Animal
type AnimalHandler = (a: Animal) => void;
// A function that handles only Dogs
type DogHandler = (d: Dog) => void;

// Without strictFunctionTypes — this unsound assignment compiles:
let animalHandler: AnimalHandler = (a: Animal) => console.log(a.name);
let dogHandler: DogHandler = animalHandler; // ❌ unsound but allowed!

// At runtime, dogHandler could receive a non-Dog animal:
dogHandler({ name: 'Cat' }); // no bark() method — would crash!

// With strictFunctionTypes:
// ❌ Error: Type 'AnimalHandler' is not assignable to type 'DogHandler'.
// 'Animal' is not assignable to parameter type 'Dog' (contravariance).
strictBindCallApply

Without this flag, bind, call, and apply accept any arguments. With it, TypeScript checks that you pass the correct argument types.

TS
function greet(greeting: string, name: string): string {
  return `${greeting}, ${name}!`;
}

// Without strictBindCallApply — wrong types compile silently
greet.call(null, 42, true);   // no error, but wrong types

// With strictBindCallApply:
// ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string'
greet.call(null, 42, true);

// ✅ Correct usage
greet.call(null, 'Hello', 'Alice');    // "Hello, Alice!"
greet.apply(null, ['Hi', 'Bob']);      // "Hi, Bob!"

const boundGreet = greet.bind(null, 'Hey'); // (name: string) => string
boundGreet('Charlie');                 // "Hey, Charlie!"
strictPropertyInitialization

This flag ensures that class properties are either initialized in the constructor or explicitly marked as possibly-undefined.

TS
// Without strictPropertyInitialization
class UserService {
  private apiUrl: string;   // no initializer — undefined at runtime
  private timeout: number;  // same

  fetchUser(id: string) {
    return fetch(this.apiUrl + id, { signal: AbortSignal.timeout(this.timeout) });
    // this.apiUrl is undefined — runtime TypeError!
  }
}

// With strictPropertyInitialization:
// ❌ Error: Property 'apiUrl' has no initializer and is not
//          definitely assigned in the constructor.

// ✅ Option 1: Initialize in constructor
class UserService {
  private apiUrl: string;
  private timeout: number;

  constructor(config: { apiUrl: string; timeout: number }) {
    this.apiUrl = config.apiUrl;
    this.timeout = config.timeout;
  }
}

// ✅ Option 2: Initialize at declaration
class UserService {
  private apiUrl = 'https://api.example.com/';
  private timeout = 5000;
}

// ✅ Option 3: Definite assignment assertion (use only when you KNOW it's set)
class UserService {
  private apiUrl!: string;  // '!' = "trust me, this gets set before use"

  init(url: string) {
    this.apiUrl = url;
  }
}
noImplicitThis

When using this inside a function, TypeScript needs to know its type. Without noImplicitThis, it silently types this as any. This flag requires you to annotate this explicitly.

TS
// Without noImplicitThis — this.name is any, no errors
function logName() {
  console.log(this.name); // this: any — silently compiles
}

// With noImplicitThis:
// ❌ Error: 'this' implicitly has type 'any' because it does not have a type annotation.

// ✅ Fix: add a 'this' parameter (it's erased at compile time, not a real parameter)
function logName(this: { name: string }) {
  console.log(this.name); // this: { name: string }
}

// ✅ Arrow functions inherit 'this' from lexical scope — no annotation needed
class Timer {
  seconds = 0;

  start() {
    setInterval(() => {
      this.seconds++; // 'this' is Timer — correctly inferred
    }, 1000);
  }
}
useUnknownInCatchVariables

Prior to TypeScript 4.4, catch clause variables were implicitly typed as any. With useUnknownInCatchVariables: true (included in strict mode from TS 4.4+), they are typed as unknown, forcing you to narrow the type before accessing properties.

TS
// Before useUnknownInCatchVariables (or TS < 4.4)
try {
  riskyOperation();
} catch (err) {
  // err: any — you can access any property without errors
  console.log(err.message); // compiles, but crashes if err is not an Error
}

// With useUnknownInCatchVariables: true
try {
  riskyOperation();
} catch (err) {
  // err: unknown — must narrow before use

  // ❌ Error: 'err' is of type 'unknown'
  console.log(err.message);

  // ✅ Narrow first
  if (err instanceof Error) {
    console.log(err.message); // string
  } else if (typeof err === 'string') {
    console.log(err);
  } else {
    console.log('Unknown error:', String(err));
  }
}

// ✅ Reusable type guard
function isError(value: unknown): value is Error {
  return value instanceof Error;
}

try {
  riskyOperation();
} catch (err) {
  if (isError(err)) console.error(err.message);
}
alwaysStrict

This flag emits "use strict"; at the top of every compiled JavaScript file and parses all files in strict mode. Modern JavaScript engines run in strict mode automatically inside ES modules, but this flag ensures compatibility with older CommonJS environments.

Migrating an Existing Codebase to Strict Mode

Enabling strict mode on a large existing codebase can produce hundreds of errors. The recommended approach is to enable individual flags one at a time, starting with the most impactful.

  1. Run tsc --noEmit and record the current error count (probably zero without strict).

  2. Add "noImplicitAny": true — fix all implicit any parameters.

  3. Add "strictNullChecks": true — this generates the most errors; tackle file by file.

  4. Add "strictPropertyInitialization": true — initialize all class properties.

  5. Add "strictFunctionTypes": true — fix function type assignments.

  6. Add "strictBindCallApply": true — fix call/bind/apply usages.

  7. Add "noImplicitThis": true — annotate this parameters.

  8. Add "useUnknownInCatchVariables": true — narrow catch clause variables.

  9. Finally, replace all individual flags with "strict": true.

JSON
// tsconfig.json — phased migration approach
{
  "compilerOptions": {
    // Phase 1 — start here
    "noImplicitAny": true,

    // Phase 2 — most impactful
    "strictNullChecks": true,

    // Phase 3 — class safety
    "strictPropertyInitialization": true,

    // Phase 4+ — the rest
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,
    "alwaysStrict": true,

    // Once all phases pass, replace everything above with:
    // "strict": true,
  }
}
Tip
Use // @ts-expect-error or // @ts-ignore sparingly as temporary markers on lines you plan to fix later. Track them with grep -r "@ts-ignore" src/ and reduce the count each sprint.
Additional Strict-Adjacent Flags Worth Enabling

These flags are not part of the strict bundle but are strongly recommended:

Flag

What it checks

noUncheckedIndexedAccess

Array/index accesses return T | undefined instead of T

exactOptionalPropertyTypes

Optional props cannot be set to undefined explicitly

noPropertyAccessFromIndexSignature

Forces bracket notation for index signature access

noImplicitOverride

Override keyword required when overriding base class methods

noFallthroughCasesInSwitch

Prevents implicit fallthrough between switch cases

noUnusedLocals

Errors on declared-but-unused local variables

noUnusedParameters

Errors on declared-but-unused function parameters

JSON
// Recommended tsconfig for new projects
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess":           true,
    "exactOptionalPropertyTypes":          true,
    "noImplicitOverride":                  true,
    "noFallthroughCasesInSwitch":          true,
    "noUnusedLocals":                      true,
    "noUnusedParameters":                  true
  }
}
Success
Projects that adopt the full recommended set of flags catch entire categories of bugs at compile time — null pointer errors, unhandled array bounds, shadowed parameters, and more — before any code runs in production.
Summary
  • "strict": true enables 8 compiler flags simultaneously — it is a shorthand, not a single check.

  • strictNullChecks is the most impactful flag: null and undefined become distinct types.

  • noImplicitAny forces explicit types on parameters that TypeScript cannot infer.

  • strictFunctionTypes enforces contravariance on function parameter types.

  • strictPropertyInitialization ensures class properties are always initialized before use.

  • useUnknownInCatchVariables types catch clause variables as unknown, forcing safe narrowing.

  • Migrate large codebases incrementally: enable one flag at a time.

  • Beyond strict mode, consider noUncheckedIndexedAccess and exactOptionalPropertyTypes for even stronger guarantees.