TypeScript Strict Mode — Enable and Fix Errors

Sanjeev SharmaSanjeev Sharma
6 min read

Advertisement

Strict mode is TypeScript's safety harness. It enforces strict null checks, implicit any errors, and function type compatibility. Enabling it transforms TypeScript from a helpful suggestion system into a powerful type-safety guarantee.

Enabling Strict Mode

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

This single setting enables eight strictness flags. You can also configure them individually:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

noImplicitAny

The most important strict flag. It prevents variables from having an implicit any type.

// ❌ Error with strict mode
function greet(name) {
  return `Hello, ${name}!`;
}

// ✅ Fixed
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// ❌ Error
const multiply = (a, b) => a * b;

// ✅ Fixed
const multiply = (a: number, b: number): number => a * b;

strictNullChecks

Prevents null and undefined from being assignable to any type except their own types.

// ❌ Error with strict mode
function printLength(str: string) {
  console.log(str.length);
}

printLength(null); // Error: null is not assignable to string

// ✅ Fixed - explicitly allow null
function printLength(str: string | null) {
  if (str === null) return;
  console.log(str.length);
}

// ✅ Or use non-null assertion (use sparingly)
printLength(maybeString!);

noImplicitThis

Prevents this from having an implicit any type.

// ❌ Error with strict mode
function processData() {
  console.log(this.value);
}

// ✅ Fixed - specify this type
function processData(this: { value: string }) {
  console.log(this.value);
}

// ✅ Or use arrow functions in classes
class DataProcessor {
  value = "data";

  process = () => {
    console.log(this.value); // this is bound
  };
}

strictFunctionTypes

Enforces stricter function type checking, especially for parameters.

// ❌ Error with strict mode
function printString(x: string) {
  console.log(x);
}

let printSomething: (x: string | number) => void = printString;
// Error: function with string param cannot be assigned to function with string | number param

// ✅ Fixed - contravariance
let printSomething: (x: string) => void = printString;

// ✅ Or use more general parameter type
function print(x: string | number) {
  console.log(x);
}

let printSomething: (x: string | number) => void = print;

strictBindCallApply

Enables strict type checking for call(), apply(), and bind().

// ❌ Error with strict mode
function greet(greeting: string, name: string) {
  console.log(greeting + " " + name);
}

const boundGreet = greet.bind(null);
boundGreet("Hello"); // Error: missing 'name' argument

// ✅ Fixed
const boundGreet = greet.bind(null, "Hello");
boundGreet("Alice"); // OK

// ✅ Or provide correct arguments
greet.call(null, "Hello", "Alice");

strictPropertyInitialization

Requires all class properties to be initialized or declared optional.

// ❌ Error with strict mode
class User {
  name: string; // Error: not initialized
  email: string; // Error: not initialized
}

// ✅ Fixed - initialize in constructor
class User {
  name: string;
  email: string;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }
}

// ✅ Or make optional
class User {
  name?: string;
  email?: string;
}

// ✅ Or use definite assignment assertion (use sparingly)
class User {
  name!: string;
  email!: string;

  initialize(name: string, email: string) {
    this.name = name;
    this.email = email;
  }
}

noImplicitReturns

Ensures all code paths return a value.

// ❌ Error with strict mode
function getValue(type: "string" | "number"): string | number {
  if (type === "string") {
    return "hello";
  } else if (type === "number") {
    return 42;
  }
  // Error: not all code paths return
}

// ✅ Fixed
function getValue(type: "string" | "number"): string | number {
  if (type === "string") {
    return "hello";
  }
  return 42;
}

// ✅ Or use switch with exhaustive checking
function getValue(type: "string" | "number"): string | number {
  switch (type) {
    case "string":
      return "hello";
    case "number":
      return 42;
    default:
      const exhaustive: never = type;
      return exhaustive;
  }
}

noFallthroughCasesInSwitch

Prevents switch cases from falling through without a break.

// ❌ Error with strict mode
switch (value) {
  case "a":
    console.log("A");
  case "b":
    console.log("B"); // Error: fallthrough
    break;
}

// ✅ Fixed
switch (value) {
  case "a":
    console.log("A");
    break;
  case "b":
    console.log("B");
    break;
}

// ✅ Or explicitly allow fallthrough
switch (value) {
  case "a":
  case "b":
    console.log("A or B");
    break;
}

Migration Strategy

Phase 1: Enable Strict Mode

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

Phase 2: Fix Errors

Start with the most impactful:

  1. noImplicitAny - add type annotations
  2. strictNullChecks - add null checks
  3. strictFunctionTypes - fix function signatures
  4. Others as they appear

Phase 3: Build Type-Safe Practices

// Create a types.ts for shared interfaces
// src/types/index.ts
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface ApiResponse<T> {
  status: "success" | "error";
  data: T;
  error?: string;
}

// Use throughout project
import type { User, ApiResponse } from "./types";

export async function getUser(id: number): Promise<ApiResponse<User>> {
  // Implementation
  return { status: "success", data: { id: 1, name: "Alice", email: "alice@example.com" } };
}

Common Patterns for Strict Mode

Handling Null/Undefined

// Type guard
function isNotNull<T>(value: T | null): value is T {
  return value !== null;
}

const values: (string | null)[] = ["a", null, "b"];
const nonNull = values.filter(isNotNull); // string[]

// Nullish coalescing
const name: string = userInput ?? "Guest";

// Optional chaining
const email: string | undefined = user?.email;

// Logical OR for truthy
const port: number = parseInt(process.env.PORT || "3000", 10);

Function Overloads

// Multiple signatures for strict type safety
function process(value: string): string;
function process(value: number): number;
function process(value: string | number): string | number {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return value * 2;
}

const str = process("hello"); // string
const num = process(42); // number

Performance Impact

Strict mode has no runtime performance impact. It's a compile-time safety feature. The only cost is development time to add types.

FAQ

Q: Can I enable strict mode gradually? A: Yes, enable individual flags or use skipLibCheck: true to ignore external library errors while you migrate your own code.

Q: What about legacy code that's hard to type? A: Use any for specific lines with a comment. Better: refactor gradually. Bad types are worse than no types.

Q: Does strict mode catch all runtime errors? A: No. TypeScript is not a runtime type checker. It catches many common errors but can't guarantee runtime safety. Use runtime validation for untrusted data.


Enabling strict mode is the best investment you can make in code quality. Yes, you'll see errors initially. But those errors represent bugs waiting to happen. Fix them now, not in production. Your future self will thank you.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro