# Why I Still Use TypeScript Strict Mode in 2025 (And You Should Too)
By Govind Bajaj · 2025

Tags: typescript, strict-mode, type-safety, debugging, best-practices
Three 'any' types in a codebase I inherited cost me 4 hours of debugging last month. Strict mode would have caught all three at compile time. The case is still open-and-shut.
I spent four hours debugging a production issue last month that TypeScript strict mode would have caught at compile time.

The culprit was three `any` types in a file written two years ago. A refactor changed a return shape from `{ id: string, name: string }` to `{ id: string, data: { name: string } }`. The `any` types swallowed the mismatch. The code compiled. The runtime blew up.

This is not a unique story. I've seen variations of it at least a dozen times. And every time, the fix is the same: turn on strict mode and delete `any` from your vocabulary.

## What Strict Mode Actually Gives You

TypeScript's strict mode is a bundle of compiler flags. The important ones:

- `strictNullChecks`: null and undefined are separate types; you must handle them explicitly
- `noImplicitAny`: parameters and variables without type annotations don't default to `any`
- `strictFunctionTypes`: function parameter types are checked contravariantly
- `noImplicitReturns`: all code paths must return a value
- `noUncheckedIndexedAccess`: indexing an array or object might return undefined

Together, they transform TypeScript from "JavaScript with autocomplete" into "a type system that actually prevents bugs."

## The Real-World Bug Strict Mode Caught

Here's the actual code from the Sans Herbals checkout flow:

```ts
// Without strictNullChecks, this compiles fine
function getUserDiscount(user: User | null) {
  return user.tier === 'premium' ? 0.2 : 0
  //     ^^^ strict mode: 'user' is possibly 'null'
}

// With strictNullChecks, you're forced to handle it
function getUserDiscount(user: User | null) {
  if (!user) return 0
  return user.tier === 'premium' ? 0.2 : 0
}
```

In the non-strict version, `user` has an implicit `any` when accessed. If the auth session expires and `user` becomes `null`, the runtime crashes with "Cannot read property 'tier' of null."

With strict mode, the compiler refuses to build until you handle the null case. The bug never reaches production.

## The 'any' Taxonomy of Sins

I've seen `any` used for three reasons, and all three are wrong:

**"I don't know the type yet"**

Use `unknown` instead. `unknown` forces you to narrow the type before using it. `any` lets you do anything, which means the compiler can't help you.

```ts
// Bad
function parseResponse(response: any) {
  return response.data.items
  // If the API changes shape, this silently breaks
}

// Good
function parseResponse(response: unknown) {
  if (typeof response === 'object' && response !== null && 'data' in response) {
    // TypeScript knows response has a 'data' property here
    return (response as { data: { items: Item[] } }).data.items
  }
  throw new Error('Invalid response shape')
}
```

**"The third-party library has no types"**

Install `@types/package-name`. If it doesn't exist, write a minimal type declaration:

```ts
// types/legacy-lib.d.ts
declare module 'legacy-lib' {
  export function init(config: { apiKey: string }): void
  export function track(event: string): void
}
```

Two lines of types beats one `any` that hides every bug.

**"It's just a temporary hack"**

Temporary hacks are permanent. Every `any` I've seen marked with a `// TODO: fix` comment has stayed for months or years. If the code compiles and ships, the TODO never gets prioritized.

## Strict Mode in a Next.js Project

Enable it in your `tsconfig.json`:

```json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}
```

`noUncheckedIndexedAccess` is not included in `strict: true` by default, but I enable it separately. It treats `array[0]` as possibly undefined, which catches more runtime errors at compile time.

## Handling the Migration Pain

If you're enabling strict mode on an existing codebase, don't do it all at once. Use `// @ts-strict-ignore` comments to gradually migrate file by file. Or use TypeScript's project references to have different strictness levels for different parts of your codebase.

But don't leave the escape hatch open indefinitely. Set a deadline. Every file with a `// @ts-ignore` or `: any` is a bug waiting to happen.

## Takeaways

- Strict mode transforms TypeScript from autocomplete into a real bug-prevention tool
- `noImplicitAny` and `strictNullChecks` catch the most common runtime errors at compile time
- Replace every `any` with `unknown`, a proper type, or a minimal type declaration
- `noUncheckedIndexedAccess` catches array indexing errors that `strict: true` misses
- The time you spend writing types is repaid 10x in debugging time you don't spend
- Temporary `any` is permanent `any` — don't use it as a shortcut