Development··3 min read

What Happened After Turning on TypeScript Strict Mode

The flood of errors that hit when we enabled strict mode on a legacy project, and how we worked through them.

1,847 Errors

One Friday, I flipped "strict": true in tsconfig.json and ran the build. 1,847 errors.

This was not a Friday afternoon task. I reverted immediately.

The project was 2 years old. It used TypeScript, but with strict: false. any was sprinkled everywhere. Null checks were hand-waved with optional chaining. Type errors were silenced with as any.

Honestly, it was comfortable. We could ship features fast, and types never got in the way. But as the project grew, runtime errors kept climbing. "Cannot read property of undefined" was hitting 20 times a day in Sentry.

Starting with strictNullChecks

Fixing 1,847 errors at once was impossible. I decided to enable strict mode's sub-options one at a time.

Started with strictNullChecks -- it accounted for 60% of the errors. 1,100 errors. A lot, but most followed the same pattern: using a potentially null value without checking first.

The most common one: const user = getUser(id); followed immediately by accessing user.name. getUser returns User | undefined, but there was no undefined check.

Found Real Bugs Along the Way

Fixing these revealed actual bugs. There were places where querying a non-existent user ID would cause the server to return a 500. Code that never would have compiled with strictNullChecks on.

It took 3 weeks to fix all 1,100. About 60-70 per day. Lots of mechanical work -- tedious, but each fix made the codebase feel safer.

(When you fix 70 null checks a day, you start null-checking in your dreams.)

The War with noImplicitAny

Next up: noImplicitAny. 450 errors. Most were event handlers -- onChange={(e) => ...} where e lacked a type like React.ChangeEvent<HTMLInputElement>.

Utility functions were another big bucket. Functions like function formatData(data) with no type on data. Fixing these made function inputs and outputs explicit, preventing callers from passing incorrect data.

The Real Challenge: Third-Party Libraries

Our own code was fixable -- just do the work. The problem was third-party libraries with incomplete type definitions. Some older libraries had @types packages that hadn't kept up with the latest version.

In those cases, I either declared types manually with declare module or used // @ts-expect-error where unavoidable. @ts-expect-error is better than as any -- you can explain why it's being suppressed in a comment, and when the types eventually get fixed, it flags the now-unnecessary suppression.

Six Weeks Later, I Opened Sentry

Full strict mode adoption took 6 weeks.

The next month's Sentry report: "Cannot read property of undefined" dropped from 20 per day to 3. Over 80% reduction.

Development speed paradoxically increased too. When modifying code, the type system shows you the blast radius, so the anxiety of "will this change break something over there?" vanished.

Turn On Strict When You Start a Project

Turn it on later, and 6 weeks of pain await. Starting a new project with strict: false is like handing your future self a 6-week homework assignment.

If strict is off in your project right now, try enabling the sub-options one at a time. Doing it all at once leads to despair, but one at a time is manageable.

Related Posts