TypeScript Strict Mode — Enable It, Fix the Errors, Ship Safer Code

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Strict mode is TypeScript's highest-leverage setting. A single line in tsconfig.json activates eight safety flags that collectively catch the most common categories of runtime bugs: null dereference, implicit any, missing return paths, function type mismatches, and uninitialized properties. Teams that enable strict mode from the start report dramatically fewer production incidents.

For backend engineers, the value is compounded. APIs deal with user-supplied data, database records, and third-party responses — all sources of unexpected null values and shape mismatches. Strict mode forces you to handle every edge case explicitly at the point where data enters your system, rather than discovering it as a 500 error.

The initial migration cost for an existing project is real — you will see errors. But every error strict mode surfaces is a latent bug. Fix them at compile time, not in your incident post-mortem.

Enabling Strict Mode

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

strict: true is a shorthand that activates all of the following flags simultaneously.

noImplicitAny

Prevents variables and parameters from silently defaulting to any when no type can be inferred.

// Without strict — silently any
function multiply(a, b) {
  return a * b; // a and b are any — no protection
}
 
// With strict — must be explicit
function multiply(a: number, b: number): number {
  return a * b;
}
 
// Common migration pattern for third-party callbacks
someArray.forEach((item: unknown) => {
  // Now you must narrow the type before using it
  if (typeof item === 'string') {
    console.log(item.toUpperCase());
  }
});

strictNullChecks

The most impactful flag. Without it, null and undefined are assignable to every type — enabling the billion-dollar null reference error.

// strictNullChecks: false — this compiles and crashes at runtime
function getUser(id: string): User {
  return db.find(id); // could be null — no warning
}
getUser('missing').email; // TypeError at runtime
 
// strictNullChecks: true — must handle the null case
function getUser(id: string): User | null {
  return db.find(id);
}
 
const user = getUser('missing');
if (!user) throw new Error('User not found');
user.email; // TypeScript knows user is not null here
 
// Useful patterns for null handling
const name = user?.profile?.displayName ?? 'Anonymous';
const port = parseInt(process.env.PORT ?? '3000', 10);

noImplicitThis

Prevents this from having an implicit any type inside functions.

// Error — TypeScript can't infer the type of 'this'
function getDisplayName() {
  return this.firstName + ' ' + this.lastName; // 'this' implicitly has type 'any'
}
 
// Fixed — declare 'this' as the first fake parameter
function getDisplayName(this: { firstName: string; lastName: string }): string {
  return `${this.firstName} ${this.lastName}`;
}
 
// Arrow functions in classes avoid the issue entirely
class UserModel {
  firstName = 'Alice';
  lastName = 'Smith';
 
  getDisplayName = (): string => {
    return `${this.firstName} ${this.lastName}`; // 'this' is always the class instance
  };
}

strictFunctionTypes

Enforces correct variance for function parameter types. Parameters must be contravariant.

type StringProcessor = (value: string) => void;
type StringOrNumberProcessor = (value: string | number) => void;
 
// Error — a function expecting only string cannot be assigned to one that accepts string | number
const processor: StringOrNumberProcessor = (value: string) => console.log(value.toUpperCase());
 
// Correct — the assigned function must handle all types the variable type allows
const processor: StringOrNumberProcessor = (value: string | number) => console.log(String(value));

strictPropertyInitialization

All class properties must be initialized in the constructor or declared with a definite assignment assertion.

// Error — properties are declared but never assigned
class UserService {
  db: Database;          // Error: not initialized
  logger: Logger;        // Error: not initialized
}
 
// Fixed — initialize in constructor
class UserService {
  private readonly db: Database;
  private readonly logger: Logger;
 
  constructor(db: Database, logger: Logger) {
    this.db = db;
    this.logger = logger;
  }
}
 
// For lazy initialization — use definite assignment assertion sparingly
class ConfigService {
  private config!: Record<string, string>; // ! means "I guarantee this is set before use"
 
  async load(): Promise<void> {
    this.config = await loadConfigFromFile();
  }
}

strictBindCallApply

Ensures bind, call, and apply receive correctly typed arguments.

function formatCurrency(amount: number, currency: string): string {
  return `${currency}${amount.toFixed(2)}`;
}
 
// Error — missing the 'currency' argument
const bound = formatCurrency.bind(null, 100);
bound(); // Error: Expected 1 arguments, but got 0
 
// Correct
const bound = formatCurrency.bind(null, 100);
bound('$'); // OK — provides the missing 'currency' argument

noImplicitReturns and noFallthroughCasesInSwitch

// noImplicitReturns — all code paths must return
function classify(score: number): 'pass' | 'fail' | 'distinction' {
  if (score >= 90) return 'distinction';
  if (score >= 60) return 'pass';
  // Error: Not all code paths return a value
}
 
// Fixed
function classify(score: number): 'pass' | 'fail' | 'distinction' {
  if (score >= 90) return 'distinction';
  if (score >= 60) return 'pass';
  return 'fail';
}
 
// noFallthroughCasesInSwitch
function handleStatus(status: string): void {
  switch (status) {
    case 'active':
      console.log('User is active');
      // Error: Fallthrough case in switch
    case 'pending':
      console.log('Needs review');
      break;
  }
}

Incremental Migration Strategy

For existing codebases, enable flags one at a time rather than all at once.

{
  "compilerOptions": {
    "noImplicitAny": true
  }
}

Recommended order:

  1. noImplicitAny — biggest return on investment
  2. strictNullChecks — catches null dereference bugs
  3. strictPropertyInitialization — enforces DI patterns
  4. strictFunctionTypes — usually few errors
  5. Enable strict: true to catch remaining flags
// Use 'unknown' during migration for untyped external data
function processWebhook(payload: unknown): void {
  // Narrow the type before use
  if (
    typeof payload === 'object' &&
    payload !== null &&
    'event' in payload &&
    typeof (payload as { event: unknown }).event === 'string'
  ) {
    console.log((payload as { event: string }).event);
  }
}

Common Mistakes

  • Using ! (non-null assertion) too liberally to silence strict errors instead of fixing them
  • Enabling strict: true without reviewing every error — some indicate real bugs
  • Using any as a migration shortcut and never coming back to fix it
  • Disabling strictNullChecks because it surfaces many errors — those are real bugs
  • Forgetting to update .d.ts declaration files when fixing strict mode errors in library code

Best Practices

  • Always start new projects with strict: true — the cost is near zero at project inception
  • Use eslint-plugin-@typescript-eslint with no-explicit-any to enforce no any in code review
  • Treat // @ts-ignore and // @ts-expect-error as technical debt that needs a linked issue
  • Write type guards for all unknown data from external sources (HTTP requests, database rows, env vars)
  • Use exhaustive checking with never for switch statements over union types

Key Takeaways

  • strict: true is a single tsconfig flag that enables eight compile-time safety checks
  • strictNullChecks is the most impactful flag — it eliminates entire classes of null dereference errors
  • noImplicitAny forces explicit type annotations, making function contracts obvious and auditable
  • strictPropertyInitialization requires constructor injection patterns, improving testability
  • noImplicitReturns catches missing return paths in functions with complex control flow
  • All strict mode flags are compile-time only — they have zero runtime performance impact
  • For existing projects, migrate flag by flag starting with noImplicitAny and strictNullChecks
  • Every error strict mode surfaces is a latent bug — treat them as value, not friction

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading