close
Skip to main content

TypeScript Strict Mode Explained

"strict": true in your tsconfig.json is a bundle of seven independent flags. This guide explains what each one does, the kind of bug it catches, and the order we recommend enabling them during a JavaScript-to-TypeScript migration.

noImplicitAny

Forbids parameters and variables from silently being typed as any. You either annotate them or let inference do the work — never both nothing.

Off — silently passes
function greet(name) {  // name: any
  return "Hello " + name.toUppercase();
  // typo not caught at compile time
}
On — compile error
function greet(name) {
  // TS7006: Parameter 'name' implicitly has an 'any' type.
}

strictNullChecks

The big one. null and undefined are distinct types — they no longer assign to other types implicitly. Catches the entire class of "cannot read property X of undefined" runtime errors at compile time.

function findUser(id: number): User | undefined { /* ... */ }

const u = findUser(42);
u.name;        // TS2532: Object is possibly 'undefined'.
u?.name;       // OK — optional chaining
if (u) u.name; // OK — narrowed by guard

strictFunctionTypes

Checks function parameter compatibility contravariantly. Plain English: a callback that expects a more specific type cannot be passed where a callback that gets a more general type is expected.

type AnimalCb = (a: Animal) => void;
type DogCb    = (d: Dog) => void;

declare let dogCb: DogCb;
const animalCb: AnimalCb = dogCb;
// TS2322: Type '(d: Dog) => void' is not assignable to type 'AnimalCb'.

Usually harmless; surprising the first time you see it. The fix is almost always to make the callback accept the more general type.

strictBindCallApply

Function.prototype.bind, .call, and .apply require correct argument types. Without this flag they accept anything.

function greet(name: string, salutation: string) { /* ... */ }
greet.call(null, "Ada", 42);
// TS2769: Argument of type 'number' is not assignable to parameter of type 'string'.

strictPropertyInitialization

Class fields must be initialized in the constructor (or marked with the ! definite-assignment assertion). Catches "I forgot to set this in the constructor" bugs.

class User {
  name: string;           // TS2564: Property 'name' has no initializer
  age!: number;           // OK: definite-assignment assertion
  role: string = "user";  // OK: default value
  constructor() {}        // doesn't set name
}

noImplicitThis

Functions whose this is implicit any error. Annotate this explicitly or use arrow functions to inherit lexical this.

function handler() {
  this.value;
  // TS2683: 'this' implicitly has type 'any'.
}

function handler(this: { value: number }) {
  this.value; // OK
}

alwaysStrict

Emits "use strict" at the top of every emitted JavaScript file. ES modules are already strict, so this only changes output if you target non-module scripts. Safe to leave on.

Recommended enable order during migration

Turning "strict": true on day one of a migration floods your team with errors. Instead, enable flags one at a time in this order:

  1. noImplicitAny — flushes out untyped parameters.
  2. strictNullChecks — the biggest correctness win. Takes the longest.
  3. strictBindCallApply — usually a few quick fixes.
  4. strictFunctionTypes — rare in practice.
  5. noImplicitThis — catches dangling this in legacy code.
  6. strictPropertyInitialization — loudest; enable last after classes settle.
  7. alwaysStrict — safe to enable anytime.

Migrating to strict TypeScript?

JavaScriptConverter does the rename. The strict-mode pass after is yours, and this guide is your map.