Resourcesโ€บTypeScript Tipsโ€บTypeScript Strict Mode: What It Catches and Why You Need It
๐Ÿ“˜TypeScript Tipsโ€” TypeScript Strict Mode: What It Catches and Why You Need Itโฑ 6 min

TypeScript Strict Mode: What It Catches and Why You Need It

TypeScript's strict mode enables 7 checks that catch real bugs. Here's exactly what each one does and why they matter.

๐Ÿ“…January 18, 2026โœTechTwitter.iotypescriptstrict-modesafety

What strict: true Actually Enables

Most TypeScript tutorials say "enable strict mode." Few explain what it turns on. strict: true in tsconfig.json is a shorthand for enabling 7 separate compiler flags:

{
  "compilerOptions": {
    "strict": true
    // Equivalent to:
    // "noImplicitAny": true,
    // "strictNullChecks": true,
    // "strictFunctionTypes": true,
    // "strictBindCallApply": true,
    // "strictPropertyInitialization": true,
    // "noImplicitThis": true,
    // "useUnknownInCatchVariables": true
  }
}

Each one catches a different class of bug. Here's what they do.


1. noImplicitAny โ€” No Silent any

Without it, TypeScript infers any when it can't determine a type, silently opting out of type checking.

// Without noImplicitAny โ€” no error, but `data` is `any`
function process(data) {
  return data.length // Could crash at runtime
}

// With noImplicitAny โ€” error: Parameter 'data' implicitly has an 'any' type
// Forces you to be explicit:
function process(data: string | unknown[]) {
  return data.length
}

This is the single most important check. Implicit any is how TypeScript becomes JavaScript with extra steps.


2. strictNullChecks โ€” null Is Not a Value

Without this, null and undefined are assignable to any type. This is the source of most TypeScript runtime crashes.

// Without strictNullChecks โ€” valid code, crashes at runtime
const user: User = getUser() // might return null
console.log(user.name) // TypeError: Cannot read properties of null

// With strictNullChecks โ€” caught at compile time
const user: User | null = getUser()
if (user) {
  console.log(user.name) // Safe
}

3. strictFunctionTypes โ€” Contravariant Parameter Checking

This prevents subtle bugs in function parameter handling:

type Handler = (event: MouseEvent) => void
type SpecificHandler = (event: ClickEvent) => void // ClickEvent extends MouseEvent

// Without strictFunctionTypes โ€” allowed, but wrong
const handler: Handler = (e: ClickEvent) => e.clientX

// With strictFunctionTypes โ€” error
// You can't substitute a handler that only works with ClickEvent
// where a handler for any MouseEvent is expected

4. strictBindCallApply โ€” Typed bind, call, apply

function greet(name: string, age: number): string {
  return `${name} is ${age}`
}

// Without strict โ€” no error
greet.call(null, 'Alice', 'not a number') // Wrong type, no error

// With strictBindCallApply โ€” error: Argument of type 'string' is not assignable to 'number'

5. strictPropertyInitialization โ€” Class Fields Must Be Initialized

class Database {
  connection: Connection // Error: property not initialized in constructor

  // Must either:
  connection!: Connection // definite assignment assertion (you guarantee it)
  // or
  connection: Connection | undefined // explicit about the possibility
  // or initialize in constructor
  constructor() {
    this.connection = createConnection()
  }
}

6. noImplicitThis โ€” No Untyped this

class Timer {
  delay = 1000

  start() {
    // Without noImplicitThis โ€” 'this' is any, crash at runtime
    setTimeout(function() {
      console.log(this.delay) // undefined โ€” wrong 'this'
    }, 100)

    // With noImplicitThis โ€” error caught, use arrow function instead
    setTimeout(() => {
      console.log(this.delay) // Correct
    }, 100)
  }
}

7. useUnknownInCatchVariables โ€” Caught Errors Are unknown

try {
  await fetchData()
} catch (error) {
  // Without this flag: error is 'any' โ€” you can access any property
  console.log(error.message) // Might not exist

  // With this flag: error is 'unknown' โ€” must narrow first
  if (error instanceof Error) {
    console.log(error.message) // Safe
  }
}

Migrating an Existing Codebase

Enabling strict mode on an existing codebase can surface hundreds of errors. The pragmatic approach:

{
  "compilerOptions": {
    "strict": false,
    // Enable flags one at a time
    "noImplicitAny": true,
    "strictNullChecks": true
    // Add more as you fix errors
  }
}

Start with noImplicitAny and strictNullChecks โ€” they catch the most bugs. Add the rest incrementally.


Key Takeaways

  • strict: true enables 7 separate checks โ€” not just "be stricter"
  • strictNullChecks and noImplicitAny catch the most real-world bugs
  • Migrate incrementally: enable flags one at a time on existing projects
  • New projects: always start with strict: true โ€” retroactive migration is painful