Our App was created in 2017. It is a React application written in TypeScript. At the time, TypeScript was gaining popularity, but strict type safety wasn’t a major concern for most teams. Our knowledge of TypeScript was limited, and the primary goal was to use it for basic type annotations rather than enforcing a fully type-safe codebase.
As a result, strict mode was never turned on. The codebase ran without the safeguards strict mode brings: strict null checks, no implicit any, and tighter type inference. As the project grew, that gap started to cost us. Subtle bugs slipped through, and refactoring carried more risk than it should have.
The project is now around 2,270 TypeScript files, which makes a migration to strict mode a serious undertaking. We did it anyway, because better type safety, easier maintenance, and fewer runtime errors were worth more than the cost of the migration. This post is about how we made that transition manageable and what got in the way.
The Challenge of Enabling Strict Mode
Turning on strict mode in tsconfig.json immediately produced over 6,000 TypeScript errors across the codebase. We expected a lot, but the number was still daunting. Fixing every error before doing anything else wasn’t a real option, for a few reasons:
- Addressing all these issues at once would have meant halting new feature development and improvements for weeks, if not months. Given the scale of the project, such a pause was not an option.
- Many parts of the application had been running without issues for years. Fixing these areas would provide little immediate value and could even introduce unnecessary risk.
- Forcing the entire team to stop their current work and focus solely on type fixes would have been demotivating and inefficient.
- A full migration needed a gradual, non-disruptive approach, one that let the team keep delivering while improving type safety incrementally. The next step was finding a way to turn strict mode on without breaking everything at once.
A Gradual Transition Approach
With strict mode on, we still had to keep shipping while we dug out from under the errors. Rather than fix all 6,000+ at once, we suppressed them in a controlled way and chipped away at them over time.
We wrote a Python script to do the suppression:
- The script runs
tscwith strict mode enabled and captures all compilation errors in a log file. - It processes the error output to determine which files and lines contain type issues.
- It automatically opens each file with errors and adds
TODO: @ts-expect-errorcomments at the appropriate locations.
With those comments in place, the whole project compiled again with strict mode on. That gave us room to:
- Continue feature development without being blocked by TypeScript errors.
- Incrementally clean up
@ts-expect-errorcomments, prioritizing areas of the codebase that change frequently. - Ensure new code follows strict mode standards while legacy parts remain untouched until needed.
The script we used can be found here: AnnotateErrors
Enforcing Strict Mode Over Time
Alongside the script, we set a few rules so the cleanup would actually happen instead of stalling:
- If a developer needed to modify a file that contained
@ts-expect-errorannotations, they were encouraged to remove or fix them whenever possible. - Any newly written code had to fully comply with strict mode rules from the start, ensuring that the project didn’t accumulate new technical debt.
- As developers worked on different parts of the codebase, they could prioritize fixing type issues in frequently updated areas while leaving stable, rarely touched files alone.
This approach allowed us to enforce strict mode without requiring a massive cleanup effort upfront. Over time, the number of @ts-expect-error annotations dropped on its own as people fixed them while working on features or bug fixes. Here’s an example of an annotated block:
const listingKeysForAutoupdate =
data?.quoteList?.quoteList?.edges?.length > 0 &&
data.quoteList.quoteList.edges
/* @ts-expect-error TODO: TS7031 => Binding element 'node' implicitly has an 'any' type. */
.map(({ node }) => {
return {
listingKey: node?.instrumentKey,
isMarketOpen: node.isMarketOpen,
};
})
/* @ts-expect-error TODO: TS7006 => Parameter 'key' implicitly has an 'any' type. */
.filter((key) => key !== null);