TypeScript Strict Mode: The Pain Is Worth It
Published
15 min read
Apr 28, 2025

Engineering

TypeScript Strict Mode: The Pain Is Worth It

TypeScriptEngineeringDXMigration

Overview

We turned on strict mode across a 60k-line codebase. Here's every error we hit, how we fixed them, and why we'd do it again.

TypeScriptEngineeringDXMigration
Jordan Mercer

Jordan Mercer

Author
15 min read
Reading Time

TypeScript Strict Mode: The Pain Is Worth It

The 72-Hour Migration That Saved 500 Hours

When we turned on strict: true across our 60,000-line codebase, TypeScript screamed at us with 847 errors. Three days later, we had a codebase that caught bugs before they reached production — and we'd never go back.

TypeScript compilation processTypeScript compilation process

What Strict Mode Actually Does

The strict flag enables five compiler flags that catch entire classes of bugs:

json
{
  "compilerOptions": {
    "strict": true,
    // These all become true:
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true
  }
}

The Most Common Errors (And How We Fixed Them)

Error #1: Implicit Any

typescript
// Before (Error: Parameter 'data' implicitly has an 'any' type)
function processData(data) {
  return data.value
}

// After
interface DataPacket {
  value: string
  timestamp: Date
}

function processData(data: DataPacket) {
  return data.value
}

Error visualization showing any typesError visualization showing any types

Error #2: Null/Undefined Everywhere

typescript
// Before (Error: Object is possibly 'null')
const user = getUser()
console.log(user.name)

// After — Option 1: Guard clause
const user = getUser()
if (user) {
  console.log(user.name)
}

// After — Option 2: Optional chaining
console.log(user?.name)

// After — Option 3: Nullish coalescing
console.log(user?.name ?? 'Anonymous')

Error #3: Uninitialized Properties

typescript
// Before (Error: Property 'id' has no initializer)
class User {
  id: number
  name: string
}

// After — Definite assignment assertion
class User {
  id!: number
  name!: string
}

// Better — Constructor initialization
class User {
  constructor(
    public id: number,
    public name: string
  ) {}
}

Strategic Migration Approach

Phase 1: Low-Hanging Fruit (Week 1)

Start with the easiest fixes:

  1. Add explicit types to function parameters
  2. Add return types to public functions
  3. Fix low-level utility files first
bash
# Find files with most errors
npx typescript --noEmit --pretty false |   grep "error TS" |   cut -d'(' -f1 |   sort | uniq -c | sort -rn | head -10

Phase 2: The Nulls (Week 2-3)

Create helper types for common patterns:

typescript
type Nullable<T> = T | null
type Optional<T> = T | undefined
type Maybe<T> = T | null | undefined

function safeGet<T>(value: Maybe<T>, fallback: T): T {
  return value ?? fallback
}

Phase 3: Complex Types (Week 4)

Use TypeScript's advanced types:

typescript
// Discriminated unions for state machines
type RequestState = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success', data: User[] }
  | { status: 'error', error: string }

// Template literal types
type EventName = `on${Capitalize<keyof HTMLElementEventMap>}`
// Results in: 'onClick' | 'onMouseOver' | 'onSubmit' ...

Discriminated union type diagramDiscriminated union type diagram

The Hidden Benefits

1. Self-Documenting Code

typescript
// Before — What does this return?
function calculate(x, y) {
  // ... 50 lines of math
}

// After — You know exactly what to expect
function calculate(x: number, y: number): CalculationResult {
  // ... implementation
}

2. Refactoring Without Fear

With strict mode, renaming a property breaks everywhere it's used — which is exactly what you want. No silent failures.

3. Better IDE Support

VSCode's IntelliSense becomes psychic. Autocomplete shows you exactly what's available, never guessing wrong.

The Numbers Don't Lie

Before strict mode (3 months):

  • Production errors: 47
  • Average debug time: 4 hours
  • "Cannot read property of undefined" errors: 12

After strict mode (3 months):

  • Production errors: 8
  • Average debug time: 45 minutes
  • "Cannot read property of undefined" errors: 0

What We'd Do Differently

  1. Start strict from day one on new projects
  2. Use unknown instead of any for gradual typing
  3. Enable exactOptionalPropertyTypes for stricter optional handling
  4. Automate with tsc --noEmit in CI

Your Turn: The 5-Day Plan

Day 1: Enable strict: true and count errors Day 2-3: Fix all function parameter/return types Day 4: Handle null/undefined with guard clauses Day 5: Run tsc --noEmit in CI

The first week hurts. The second week gets better. By week three, you'll wonder how you ever coded without it.

Strict TypeScript isn't about being pedantic — it's about catching bugs before your users do. And that's worth every error message.

Jordan Mercer

Written by

Jordan Mercer

Let's work together

Start your project.