TypeScript Strict Mode: All Strict Compiler Options

TypeScript's strict mode isn't a single feature—it's a collection of eight compiler flags that enforce rigorous type checking. When you set `'strict': true` in your `tsconfig.json`, you're enabling...

Key Insights

  • TypeScript’s strict flag is a meta-option that enables eight individual compiler flags, each catching different categories of type safety issues that would otherwise slip through at compile time
  • strictNullChecks and noImplicitAny provide the most immediate value by eliminating the two most common sources of runtime errors: null reference exceptions and untyped variables silently becoming any
  • Enable strict mode from day one on new projects; for existing codebases, enable individual flags incrementally starting with noImplicitAny and strictNullChecks to avoid overwhelming your team with hundreds of errors

Understanding TypeScript’s Strict Mode

TypeScript’s strict mode isn’t a single feature—it’s a collection of eight compiler flags that enforce rigorous type checking. When you set "strict": true in your tsconfig.json, you’re enabling all of them simultaneously. Understanding each flag individually helps you write safer code and troubleshoot type errors more effectively.

Here’s what enabling strict mode actually does:

{
  "compilerOptions": {
    "strict": true
  }
}

This is equivalent to:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "useUnknownInCatchVariables": true
  }
}

Each flag addresses a specific category of type safety issues. Let’s examine them individually.

noImplicitAny: Eliminate Type Ambiguity

Without noImplicitAny, TypeScript allows variables and parameters to implicitly become any when it cannot infer their types. This defeats the entire purpose of using TypeScript—you lose type safety exactly where you need it most.

// Error with noImplicitAny enabled
function calculateTotal(price, quantity) {
  // Parameter 'price' implicitly has an 'any' type
  // Parameter 'quantity' implicitly has an 'any' type
  return price * quantity;
}

// Correct: explicit types
function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

// Also correct: TypeScript can infer the type
const items = [1, 2, 3];
items.map(item => item * 2); // 'item' is inferred as number

Enable this flag first when migrating existing projects. It forces you to think about your data structures and catches the most egregious type safety violations.

strictNullChecks: Handle Null and Undefined Explicitly

This flag separates null and undefined from all other types. Without it, every type implicitly includes null and undefined, making it impossible to catch null reference errors at compile time.

interface User {
  name: string;
  email: string;
}

function getUser(id: string): User | null {
  // Might return null if user not found
  return null;
}

// Error with strictNullChecks
const user = getUser("123");
console.log(user.email); // Object is possibly 'null'

// Correct: handle null explicitly
const user = getUser("123");
if (user !== null) {
  console.log(user.email);
}

// Or use optional chaining
console.log(user?.email);

// Or provide a default
const email = user?.email ?? "no-email@example.com";

This flag catches one of the most common sources of runtime errors. Tony Hoare, who invented null references, called them his “billion-dollar mistake.” strictNullChecks helps you avoid paying that price.

strictFunctionTypes: Enforce Sound Function Compatibility

This flag enables stricter checking of function parameter types, specifically enforcing contravariance for parameters. Without it, TypeScript uses a bivariant comparison that can allow unsafe function assignments.

interface Animal {
  name: string;
}

interface Dog extends Animal {
  breed: string;
}

// Without strictFunctionTypes, this would be allowed
let handleAnimal: (animal: Animal) => void;
let handleDog: (dog: Dog) => void = (dog) => {
  console.log(dog.breed.toUpperCase());
};

// Error with strictFunctionTypes enabled
handleAnimal = handleDog;
// Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'

// Why? Because this would crash:
handleAnimal({ name: "Generic Animal" }); // No 'breed' property!

The rule is: function A can be assigned to function B if A’s parameters are the same or more general than B’s parameters (contravariance). This prevents runtime errors from accessing properties that don’t exist.

Note: This flag doesn’t apply to method signatures for historical reasons related to DOM types. It only affects function type expressions.

strictBindCallApply: Type-Check Function Methods

JavaScript’s bind, call, and apply methods are notoriously error-prone. This flag makes TypeScript check that you’re using them correctly.

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

// Error with strictBindCallApply
greet.call(undefined, "Hello"); // Expected 2 arguments, but got 1

greet.apply(undefined, ["Hello"]); // Expected 2 arguments, but got 1

// Correct usage
greet.call(undefined, "Hello", "World");
greet.apply(undefined, ["Hello", "World"]);

const boundGreet = greet.bind(undefined, "Hello");
boundGreet("World"); // "Hello, World!"

Before this flag existed, TypeScript couldn’t verify argument counts or types for these methods. Now it can, preventing a common class of runtime errors.

Additional Strict Flags

strictPropertyInitialization

This flag requires class properties to be initialized either in their declaration or in the constructor. It works in conjunction with strictNullChecks.

class User {
  name: string; // Error: Property 'name' has no initializer
  email: string = ""; // OK: initialized
  id: string; // OK if initialized in constructor

  constructor(id: string) {
    this.id = id;
  }
}

// If you genuinely need an uninitialized property, use definite assignment assertion
class DatabaseConnection {
  connection!: Connection; // "I promise this will be initialized"

  async initialize() {
    this.connection = await createConnection();
  }
}

noImplicitThis

Prevents using this in functions where its type cannot be inferred. Particularly useful for standalone functions and callbacks.

// Error with noImplicitThis
const config = {
  apiUrl: "https://api.example.com",
  getUrl: function() {
    return this.apiUrl; // 'this' implicitly has type 'any'
  }
};

// Correct: specify 'this' type
interface Config {
  apiUrl: string;
  getUrl(this: Config): string;
}

const config: Config = {
  apiUrl: "https://api.example.com",
  getUrl: function(this: Config) {
    return this.apiUrl; // OK
  }
};

alwaysStrict

Ensures all files are parsed in ECMAScript strict mode and emits "use strict" in the output. This is mostly redundant with modern JavaScript but ensures consistency.

useUnknownInCatchVariables

Changes the default type of catch clause variables from any to unknown, forcing you to validate error types before using them.

try {
  riskyOperation();
} catch (error) {
  // With useUnknownInCatchVariables, error is 'unknown'
  console.log(error.message); // Error: Object is of type 'unknown'

  // Correct: validate the type
  if (error instanceof Error) {
    console.log(error.message);
  }
}

Migration Strategy for Existing Projects

Don’t enable all strict flags at once in an existing codebase—you’ll be overwhelmed with errors. Use this incremental approach:

{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true
  }
}

Fix all errors, then enable the next flag:

{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

Continue this process until all flags are enabled, then switch to "strict": true for simplicity.

For large codebases, use // @ts-expect-error comments to suppress errors temporarily while you work through them systematically:

// @ts-expect-error - TODO: Add proper types (Ticket #1234)
function legacyFunction(data) {
  return data.process();
}

The Bottom Line

For new projects, enable strict mode from day one. There’s no good reason not to—it catches bugs at compile time instead of production, and the code you write will be more maintainable.

For existing projects, the migration effort is worth it. Teams consistently report that enabling strict mode catches real bugs and improves code quality. Start with noImplicitAny and strictNullChecks—they provide the most value with the least disruption.

TypeScript’s strict mode isn’t about making your life harder. It’s about making the compiler work harder so your application works better. The upfront cost of explicit types pays dividends every time you refactor, every time a new team member joins, and every time you avoid a production incident caused by a null reference or type mismatch.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.