TypeScriptStrictness Flags Explained

Strictness Flags Explained

TypeScript ships with dozens of compiler flags. A subset of them are called strictness flags — they go beyond basic type checking to catch logic errors, unsafe patterns, and code smells that would otherwise compile without complaint.

This page covers every major strictness flag in depth: what it checks, what errors it surfaces, and how to fix them.

The Strict Family (enabled by "strict": true)

Flag

Since

What it catches

noImplicitAny

TS 1.0

Parameters/variables with implicit any type

strictNullChecks

TS 2.0

null/undefined used as non-nullable types

strictFunctionTypes

TS 2.6

Unsound function type assignments

strictBindCallApply

TS 3.2

Wrong argument types in bind/call/apply

strictPropertyInitialization

TS 2.7

Uninitialized class properties

noImplicitThis

TS 2.0

this typed as any inside functions

useUnknownInCatchVariables

TS 4.4

Catch clause variables typed as unknown

alwaysStrict

TS 2.1

Emits "use strict" in output JS

noImplicitAny in Depth

TypeScript falls back to any when it cannot infer a type. noImplicitAny treats this fallback as an error, forcing you to annotate explicitly.

TS
// ❌ All of these trigger noImplicitAny

// 1. Unannotated parameter
function process(data) { return data; }

// 2. Unannotated destructured parameter
function render({ id, label }) { return id + label; }

// 3. Array reduce without type
const result = [1, 2, 3].reduce((acc, n) => acc + n, 0);
// ^ Fine, TypeScript infers acc: number. But:
const grouped = items.reduce((acc, item) => {
  acc[item.key] = item.value; // ❌ acc implicitly any
  return acc;
}, {});

// ✅ Fixes

function process(data: string): string { return data; }

function render({ id, label }: { id: number; label: string }) { ... }

const grouped = items.reduce<Record<string, string>>((acc, item) => {
  acc[item.key] = item.value;
  return acc;
}, {});
strictNullChecks in Depth

With strict null checks enabled, null and undefined are separate types that must be handled explicitly. This prevents the most common class of runtime error — the dreaded "Cannot read property of null/undefined".

TS
// Common patterns that strictNullChecks forces you to handle:

// 1. DOM queries
const el = document.getElementById('app'); // HTMLElement | null
el.style.display = 'none'; // ❌ Object is possibly null

// ✅ Options
el?.style.display = 'none';                    // optional chaining
if (el) el.style.display = 'none';             // type guard
(el as HTMLElement).style.display = 'none';    // assertion (if you're certain)

// 2. Array find
const user = users.find(u => u.id === id); // User | undefined

user.name; // ❌ Object is possibly undefined
user?.name; // ✅ string | undefined

// 3. Optional object properties
interface Config {
  timeout?: number;  // number | undefined
}

function withTimeout(config: Config) {
  const ms = config.timeout ?? 5000; // nullish coalescing
  return ms * 2;
}

// 4. Map.get
const cache = new Map<string, User>();
const cached = cache.get('key'); // User | undefined

if (cached) {
  console.log(cached.name); // ✅ User
}
Tip
Prefer nullish coalescing (??) over OR (||) when dealing with nullable values — || treats 0, '', and false as falsy even when they are valid values.
strictFunctionTypes in Depth

Function type compatibility is subtle. TypeScript uses structural typing, meaning types are compatible if their shapes match. But for function parameters, structural typing alone is not safe.

strictFunctionTypes enforces contravariance for function parameters (a subtype relationship in the reverse direction) and covariance for return types.

TS
// Contravariance: a handler for a supertype can substitute a handler for a subtype
// Covariance: a function returning a subtype can substitute one returning a supertype

class Animal { name = ''; }
class Dog extends Animal { breed = ''; }
class Cat extends Animal { indoor = true; }

// A generic event handler type
type Handler<T> = (event: T) => void;

const animalHandler: Handler<Animal> = (a) => console.log(a.name);
const dogHandler: Handler<Dog> = (d) => console.log(d.breed);

// Without strictFunctionTypes (WRONG — compiles but unsound):
let handler: Handler<Animal> = dogHandler;
// Now if we call handler with a Cat — dogHandler tries to access .breed on Cat!

// With strictFunctionTypes — the compiler correctly rejects this:
// ❌ Error: Type 'Handler<Dog>' is not assignable to type 'Handler<Animal>'

// But this direction is safe (and allowed):
let dogHandlerVar: Handler<Dog> = animalHandler;
// A handler that accepts any Animal can certainly handle a Dog
Note
strictFunctionTypes applies only to function types written in function syntax ((x: T) => U), not to methods. Methods still use bivariant checking for backwards compatibility with class hierarchies.
strictBindCallApply in Depth

bind, call, and apply are notoriously difficult to type correctly. This flag checks argument types at each call site.

TS
function add(a: number, b: number): number {
  return a + b;
}

// Without strictBindCallApply — all of these compile:
add.call(null, 'hello', 'world'); // wrong types, no error!
add.apply(null, [1]);             // too few args, no error!
add.bind(null, 1, 2, 3);         // too many args, no error!

// With strictBindCallApply:
// ❌ Error: Argument of type 'string' is not assignable to 'number'
add.call(null, 'hello', 'world');

// ❌ Error: Expected 2 arguments, but got 1
add.apply(null, [1]);

// ❌ Error: Expected 2 arguments, but got 3 (for bind)
add.bind(null, 1, 2, 3);

// ✅ Correct usages
add.call(null, 1, 2);           // 3
add.apply(null, [10, 20]);      // 30
const add10 = add.bind(null, 10); // (b: number) => number
add10(5);                        // 15
strictPropertyInitialization in Depth

TypeScript checks that every non-optional class property is definitely assigned a value by the end of the constructor. This prevents a common source of runtime TypeErrors.

TS
// Common patterns and their fixes:

// Pattern 1: Constructor injection (recommended)
class Database {
  private connectionString: string;

  constructor(connectionString: string) {
    this.connectionString = connectionString;
    // or use parameter properties:
  }
}

// Pattern 2: Parameter properties (shorthand)
class Database {
  constructor(private readonly connectionString: string) {}
}

// Pattern 3: Initializer
class Config {
  retries = 3;
  timeout = 5000;
  baseUrl = 'https://api.example.com';
}

// Pattern 4: Lazy initialization with definite assignment assertion
class LazyService {
  private client!: HttpClient; // '!' asserts you'll set this before use

  async initialize() {
    this.client = await HttpClient.connect();
  }

  async fetch(url: string) {
    // Responsibility is on you to call initialize() first
    return this.client.get(url);
  }
}

// Pattern 5: Optional property (allow undefined)
class UserPreferences {
  theme?: 'light' | 'dark';     // number | undefined — no initializer needed
  language?: string;
}
noImplicitThis in Depth

Functions that reference this need a clear type for it. noImplicitThis errors when this would be implicitly typed as any.

TS
// Common scenario: callback functions
const obj = {
  value: 42,
  getValue: function () {
    return this.value; // 'this' is the object — TypeScript infers this
  },
};

// Problematic: function used as callback loses 'this'
class Counter {
  count = 0;

  getIncrementer() {
    return function increment() {
      // ❌ Error with noImplicitThis: 'this' implicitly has type 'any'
      this.count++; // 'this' is undefined in strict mode at runtime!
    };
  }

  // ✅ Fix 1: Arrow function (lexically captures 'this')
  getIncrementer() {
    return () => {
      this.count++; // 'this' is Counter
    };
  }

  // ✅ Fix 2: Explicit 'this' parameter
  increment(this: Counter) {
    this.count++; // 'this' is Counter
  }
}

// ✅ Fix 3: bind at the call site
const counter = new Counter();
const inc = counter.increment.bind(counter);
inc(); // 'this' is bound to counter
useUnknownInCatchVariables in Depth

JavaScript allows throwing any value — strings, numbers, objects, Errors. Before TypeScript 4.4, catch variables were implicitly typed as any, hiding this reality. With useUnknownInCatchVariables, they are unknown, which is the honest type.

TS
// Common patterns for handling unknown errors

// Pattern 1: instanceof check (works for Error subclasses)
async function fetchData(url: string) {
  try {
    const res = await fetch(url);
    return await res.json();
  } catch (err) {
    if (err instanceof Error) {
      console.error('Fetch failed:', err.message);
      throw err; // re-throw as Error
    }
    throw new Error(`Unknown fetch error: ${String(err)}`);
  }
}

// Pattern 2: Type guard utility
function toError(value: unknown): Error {
  if (value instanceof Error) return value;
  return new Error(String(value));
}

async function safeOperation() {
  try {
    await doSomethingRisky();
  } catch (err) {
    const error = toError(err);
    logError(error); // always an Error
  }
}

// Pattern 3: Discriminated catch object
// Some APIs throw plain objects like { code: string; message: string }
function isApiError(val: unknown): val is { code: string; message: string } {
  return (
    typeof val === 'object' &&
    val !== null &&
    'code' in val &&
    'message' in val
  );
}

try {
  await callApi();
} catch (err) {
  if (isApiError(err)) {
    console.log('API Error', err.code, err.message);
  } else if (err instanceof Error) {
    console.log('Network Error', err.message);
  }
}
Beyond strict: Extra Checks Worth Enabling
noUncheckedIndexedAccess

Array index access and index signature access return T | undefined instead of just T, correctly reflecting that the index might not exist.

TS
// Without noUncheckedIndexedAccess
const items = [1, 2, 3];
const first = items[0]; // number (but could be undefined for empty array!)
first.toFixed(2);        // compiles, but crashes on empty array

// With noUncheckedIndexedAccess
const first = items[0]; // number | undefined
first.toFixed(2); // ❌ Error: Object is possibly undefined

// ✅ Fix
if (first !== undefined) first.toFixed(2);
items[0]?.toFixed(2);

// Also applies to index signatures:
const map: Record<string, string> = {};
const val = map['key']; // string | undefined
val.toUpperCase(); // ❌ Error: Object is possibly undefined
exactOptionalPropertyTypes

When this flag is on, assigning undefined explicitly to an optional property is an error. The difference between "property is absent" and "property is present but set to undefined" is meaningful in JSON serialization and object spread.

TS
interface Config {
  timeout?: number; // means: may be absent, not "may be undefined"
}

// Without exactOptionalPropertyTypes
const c: Config = { timeout: undefined }; // allowed — ambiguous

// With exactOptionalPropertyTypes
// ❌ Error: Type 'undefined' is not assignable to type 'number'
//   (in the context of Config.timeout)
const c: Config = { timeout: undefined };

// ✅ The correct way to "unset" an optional property
const c: Config = {}; // simply omit it

// ✅ To allow explicit undefined, use union:
interface Config {
  timeout?: number | undefined; // explicitly opts in to undefined assignment
}
noImplicitOverride

When a subclass overrides a base class method, noImplicitOverride requires the override keyword. This prevents accidental overrides when base class methods are renamed.

TS
class Base {
  greet(name: string): string {
    return `Hello, ${name}!`;
  }
}

// Without noImplicitOverride — silent override, no indication of intent
class Derived extends Base {
  greet(name: string): string {
    return `Hi, ${name}!`; // could be accidental
  }
}

// With noImplicitOverride:
// ❌ Error: Method 'greet' will overwrite the base type method.
//          Did you mean to use the 'override' modifier?

// ✅ Fix: be explicit
class Derived extends Base {
  override greet(name: string): string {
    return `Hi, ${name}!`;
  }
}

// Safety: if base renames 'greet' to 'greeting',
// 'override greet' becomes a compile error — you can't accidentally create a new method
noUnusedLocals and noUnusedParameters

TS
// noUnusedLocals
function process(data: string[]) {
  const regex = /^[A-Z]/; // ❌ Error: 'regex' is declared but never used.
  return data.filter(s => s.length > 0);
}

// noUnusedParameters
function render(template: string, context: Record<string, unknown>) {
  // ❌ Error: 'context' is declared but its value is never read.
  return template;
}

// ✅ Fix: prefix unused params with _ to suppress the error
function render(template: string, _context: Record<string, unknown>) {
  return template;
}
Recommended tsconfig for New Projects

JSON
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "skipLibCheck": true,

    // — Strictness —
    "strict": true,
    "noUncheckedIndexedAccess":         true,
    "exactOptionalPropertyTypes":        true,
    "noImplicitOverride":                true,
    "noFallthroughCasesInSwitch":        true,
    "noUnusedLocals":                    true,
    "noUnusedParameters":                true,
    "noPropertyAccessFromIndexSignature": true
  }
}
Success
The TypeScript team's own open-source projects and most popular TypeScript frameworks (Angular, NestJS, tRPC) use the full strict set plus additional flags. Adopting this config from day one pays dividends throughout the life of a project.
Summary
  • "strict": true bundles 8 individual flags — think of it as a shorthand, not a monolith.

  • noImplicitAny forces explicit types on all parameters and uninferable variables.

  • strictNullChecks is the highest-value check: null and undefined become first-class types.

  • strictFunctionTypes enforces contravariance on function parameters, preventing unsound assignments.

  • strictPropertyInitialization ensures class properties cannot be undefined before the constructor runs.

  • useUnknownInCatchVariables types catch variables as unknown, requiring safe narrowing.

  • noUncheckedIndexedAccess and exactOptionalPropertyTypes are additional high-value flags beyond strict.

  • noImplicitOverride and noUnusedLocals/Parameters improve code clarity and maintainability.