Strict Mode Guide

TypeScript's strict mode is a collection of compiler flags that catch more potential runtime errors at compile time. Enabling "strict": true enforces null checks, disallows implicit any types, validates function parameter types, and more. For new projects, it is strongly recommended to enable strict mode from day one; for existing projects, you can migrate gradually by enabling individual flags one at a time.

Quick Start

Add a single line to your tsconfig.json to enable all strict checks. "strict": true is a meta flag equivalent to enabling every flag prefixed with strict in the table below.

{
  "compilerOptions": {
    "strict": true,
    // Bonus flags (not included in "strict"):
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true
  }
}

All Strict Mode Flags

The table below lists every flag included in "strict": true plus recommended bonus flags.

FlagWhat It DoesDefaultSince
strictMeta flag — enables all strict* options belowfalse2.3
strictNullChecksnull and undefined are no longer assignable to other typesfalse2.0
noImplicitAnyDisallows implicit any type inferencefalse1.0
strictFunctionTypesEnables contravariant function parameter checksfalse2.6
strictBindCallApplyType-checks bind, call, and apply argumentsfalse3.2
strictPropertyInitializationClass properties must be initialized in the constructorfalse2.7
noImplicitThisRaises error on implicit any-typed thisfalse2.0
alwaysStrictEmits "use strict" in every output filefalse2.1
useUnknownInCatchVariablesCatch clause variables are typed unknown instead of anyfalse4.4
noUncheckedIndexedAccessIndexed access returns T | undefinedfalse4.1
exactOptionalPropertyTypesDistinguishes optional properties from undefined valuesfalse4.4
noImplicitOverrideOverriding methods must use the override keywordfalse4.3

Note: Light-blue rows are bonus flags not included in "strict": true.

strictNullChecks — Null Safety

This is arguably the most important strict flag. When enabled, null and undefined are no longer subtypes of every type. You must use type narrowing or optional chaining (?.) to safely handle nullable values. This catches the vast majority of "Cannot read property of null" runtime errors at compile time.

❌ Bad — will error
function greet(name: string | null) {
  // Error: Object is possibly 'null'.
  console.log(name.toUpperCase());
}

const el = document.getElementById("app");
// Error: Object is possibly 'null'.
el.innerHTML = "Hello";
✅ Fix — safe access
function greet(name: string | null) {
  // Optional chaining
  console.log(name?.toUpperCase());
  // Type narrowing
  if (name !== null) {
    console.log(name.toUpperCase());
  }
}

// Non-null assertion (use only when certain)
const el = document.getElementById("app")!;
el.innerHTML = "Hello";

noImplicitAny — No Implicit Any

When TypeScript cannot infer the type of a variable or parameter, it falls back to any. With this flag enabled, that implicit fallback becomes a compile error. It forces you to add explicit type annotations to every parameter and variable, dramatically improving type safety across your codebase.

❌ Bad — implicit any
// Error: Parameter 'x' implicitly has an 'any' type.
function double(x) {
  return x * 2;
}

// Error: Parameter 'item' implicitly has an 'any' type.
const items = [1, 2, 3];
items.forEach(function (item, i) {
  // item is any here
});
✅ Fix — explicit types
function double(x: number): number {
  return x * 2;
}

// Arrow functions infer from Array<number>
const items = [1, 2, 3];
items.forEach((item: number, i: number) => {
  console.log(item);
});

// Use unknown for truly dynamic values
function parse(input: string): unknown {
  return JSON.parse(input);
}

strictFunctionTypes — Contravariant Parameters

This flag ensures function parameter types are checked contravariantly rather than bivariantly. In practice, this means a function accepting a base type cannot be assigned to a function type expecting a derived type. This prevents subtle runtime errors caused by type mismatches in callback functions.

❌ Bad — unsafe assignment
type Handler = (e: MouseEvent) => void;

// Error: Type '(e: Event) => void' is not
// assignable to type 'Handler'.
const handler: Handler = (e: Event) => {
  // e.clientX would be undefined at runtime!
  console.log(e.type);
};
✅ Fix — match parameter type
type Handler = (e: MouseEvent) => void;

const handler: Handler = (e: MouseEvent) => {
  console.log(e.clientX); // safe
};

// Or use a broader type for the variable:
type AnyHandler = (e: Event) => void;
const general: AnyHandler = (e: Event) => {
  console.log(e.type);
};

strictPropertyInitialization — Class Property Init

When enabled, non-optional class properties must have a default value or be initialized in the constructor. This prevents undefined errors from accessing uninitialized properties. This flag requires strictNullChecks to also be enabled.

❌ Bad — uninitialized property
class User {
  // Error: Property 'name' has no initializer
  // and is not definitely assigned.
  name: string;
  email: string;

  constructor() {
    // forgot to assign name and email!
  }
}
✅ Fix — three approaches
class User {
  // 1. Initialize with default
  name: string = "";

  // 2. Assign in constructor
  email: string;
  constructor(email: string) {
    this.email = email;
  }
}

class Service {
  // 3. Definite assignment assertion
  // (use only when init happens externally)
  db!: Database;
  async init() { this.db = await connect(); }
}

useUnknownInCatchVariables — Unknown in Catch

By default, the e in catch (e) is typed as any, letting you access any property without checks. With this flag, e becomes unknown, forcing type checks before use. Since JavaScript's throw can throw any value (not just Error objects), this restriction is entirely justified.

❌ Bad — unsafe catch
try {
  JSON.parse(userInput);
} catch (e) {
  // e is 'unknown', cannot access .message directly
  console.log(e.message); // Error!
}
✅ Fix — narrow the type
try {
  JSON.parse(userInput);
} catch (e: unknown) {
  if (e instanceof Error) {
    console.log(e.message); // safe
  } else {
    console.log("Unknown error:", String(e));
  }
}

// Helper function for reuse:
function getErrorMessage(e: unknown): string {
  if (e instanceof Error) return e.message;
  return String(e);
}

noUncheckedIndexedAccess — Safe Indexing

This flag adds undefined to the return type of index access operations. For example, arr[0] becomes T | undefined instead of T. This prevents runtime errors from out-of-bounds array access or missing object keys. Note: this flag is not included in "strict": true and must be enabled separately.

❌ Bad — unchecked index
const arr = [1, 2, 3];
const val = arr[5]; // val: number | undefined
// Error: Object is possibly 'undefined'.
console.log(val.toFixed(2));

const map: Record<string, number> = { a: 1 };
const x = map["missing"]; // x: number | undefined
console.log(x.toFixed(2)); // Error!
✅ Fix — check before use
const arr = [1, 2, 3];
const val = arr[5];
if (val !== undefined) {
  console.log(val.toFixed(2)); // safe
}

// Nullish coalescing for defaults
const map: Record<string, number> = { a: 1 };
const x = map["missing"] ?? 0;
console.log(x.toFixed(2)); // safe, x is number

// Destructuring with default
const [first = 0] = arr;

noImplicitOverride — Explicit Override Keyword

This flag requires the override keyword when overriding base class methods. It prevents accidentally creating new methods instead of overrides when the base class renames a method, which is especially valuable when refactoring large inheritance hierarchies.

❌ Bad — missing override
class Animal {
  move() { console.log("moving"); }
}

class Dog extends Animal {
  // Error: This member must have an 'override'
  // modifier because it overrides a member
  // in the base class 'Animal'.
  move() { console.log("running"); }
}
✅ Fix — add override
class Animal {
  move() { console.log("moving"); }
}

class Dog extends Animal {
  override move() { console.log("running"); }
}

// Also catches typos:
class Cat extends Animal {
  // Error: This member cannot have an 'override'
  // modifier because it is not declared in the
  // base class 'Animal'.
  override moev() { console.log("sneaking"); }
}

Migration Guide

Migrating a large project from non-strict to full strict mode can be daunting. Here is a proven step-by-step strategy:

  1. Start with noImplicitAny — This is the most fundamental flag and exposes areas with weak type inference. It typically produces the most errors, but fixes are straightforward: add type annotations.
  2. Then enable strictNullChecks — This is the most valuable flag. Fixes include adding null checks, using optional chaining ?., and non-null assertions !.
  3. Add remaining flags one at a time — Enable strictPropertyInitialization, strictFunctionTypes, strictBindCallApply in sequence. Add one flag at a time, fix all errors, then move to the next.
  4. Use // @ts-expect-error for temporary suppression — For complex errors you cannot fix immediately, mark them with // @ts-expect-error and revisit later. This is better than // @ts-ignore because it alerts you when the error is resolved.
  5. Finally switch to "strict": true — Once all individual flags are enabled, replace them with "strict": true to automatically include new strict flags added in future TypeScript versions.

Per-file Strict Checking

For JavaScript files, you can use // @ts-check to enable type checking on individual files:

// @ts-check

/** @type {string} */
let name = "Alice";

// Error: Type 'number' is not assignable to type 'string'.
name = 42;

Frequently Asked Questions

Should I use strict mode in new projects?

Absolutely yes. Enabling "strict": true from day one has virtually no extra cost since new code naturally conforms to strict rules. The TypeScript team and community unanimously recommend strict mode for new projects, and all major framework starters (React, Angular, Vue, Next.js) enable it by default.

How do I fix "Object is possibly null"?

There are four common approaches:

1. Type narrowing — Check with if (obj !== null) before use
2. Optional chaining — Use obj?.property for safe access
3. Nullish coalescing — Use obj ?? defaultValue for defaults
4. Non-null assertion — Use obj!, but only when you are certain the value is not null

What is the difference between "strict": true and individual flags?

"strict": true is a meta flag equivalent to enabling all strict-prefixed flags plus noImplicitAny, noImplicitThis, alwaysStrict, and useUnknownInCatchVariables. The key difference: when future TypeScript versions add new strict flags, "strict": true automatically includes them, whereas individually listed flags do not. Therefore, the end goal should always be "strict": true.

Can I enable strict mode gradually?

Absolutely. The recommended approach is to start with noImplicitAny, then add flags one at a time, fixing all errors before adding the next. For very large codebases, you can also use tools like typescript-strict-plugin to enable strict mode on a per-directory basis. The process may take weeks or months, but each step improves code quality.

What is the relationship between strictNullChecks and noUncheckedIndexedAccess?

strictNullChecks ensures types explicitly declared as T | null require null checks, but it does not affect the return types of index access. noUncheckedIndexedAccess goes further by automatically adding undefined to the return type of arr[i] and obj[key], since the compiler cannot guarantee a value exists at that index. They are complementary and should both be enabled.

Quick Reference Search