TypeScript Strict Mode: The Bugs It Catches Later

Strict mode is easy to enable and painful to retroactively satisfy. Here are the patterns that make it worth the upfront cost.

·3 min read
typescriptweb

"strict": true in tsconfig.json enables eight compiler checks at once. The first six catch bugs immediately. The last two — noImplicitAny and strictNullChecks — catch bugs six months later, when you’d otherwise be debugging production at 11pm.

The Two That Matter Most

noImplicitAny

Without this, TypeScript silently annotates anything it can’t infer as any. With it, you’re forced to either annotate explicitly or accept the annotation that the type was unknowable.

The function that bites:

function parseConfig(raw) {
  // What is `raw`? string? object? Buffer? `any` by default.
  return JSON.parse(raw);
}

With noImplicitAny, you must decide. The decision is the value.

strictNullChecks

Without it, null and undefined flow freely through every type. With it, a type T does not include null unless you write T | null.

function findUser(id: string): User | undefined {
  // ...
}

const user = findUser("abc");
user.name; // error: 'user' is possibly 'undefined'

The possibly undefined error feels pedantic until the day a database query returns null and your code throws Cannot read property 'name' of undefined.

Patterns That Make Strict Bearable

Narrow with type guards, not casts.

// Bad — silences the compiler, doesn't actually check
const name = (user as User).name;

// Good — narrows the type, runs at runtime
if (user) {
  const name = user.name;
}

Use the exhaustive switch for discriminated unions.

type Result = { ok: true; value: number } | { ok: false; error: string };

function handle(r: Result) {
  switch (r.ok) {
    case true:
      return r.value;
    case false:
      return r.error;
    default:
      const _exhaustive: never = r;
      return _exhaustive;
  }
}

The _exhaustive: never line gives you a compile-time error if you ever add a new variant to Result without updating handle. This is the single best TypeScript feature, and it only works under strict mode.

Don’t reach for ! (non-null assertion).

user!.name says “I know better than the compiler.” Sometimes you do. More often, you’ll regret it. Prefer:

if (user === undefined) throw new Error("user must exist");
user.name; // now safe

The explicit throw is debuggable. The non-null assertion isn’t.

Avoid as entirely when possible.

Type assertions are an escape hatch, not a tool. If you need to cast, ask why the type isn’t what you expect — usually the fix is upstream.

The Migration Tax

Enabling strict on an existing codebase is brutal. The strategy:

  1. Turn it on, accept the wall of errors.
  2. Pick a directory. Fix it. Commit.
  3. Repeat.

Don’t try to land strict mode in one PR. The cognitive load is too high and the review is meaningless.

Is It Worth It?

Every time. The bugs strict mode catches are the ones that take down production: undefined access on optional data, implicit any flowing through a hot path, a discriminated union that grew a new case nobody handled. The upfront tax is real; the late-stage payoff is larger.