Lessons From Migrating 200,000 Lines of JavaScript to TypeScript in Production
TypeScript migration is a long-term investment with short-term costs that are easy to underestimate. Here's what we learned from doing it with a production system and a team that had to keep shipping.
There's a specific moment in the lifecycle of most JavaScript projects where the codebase becomes difficult to work with: the moment when the implicit type contracts between functions become impossible to hold in your head, when refactoring requires touching thirty files to verify nothing breaks, and when onboarding a new engineer takes weeks because the codebase's behavior can only be understood by reading it all. We hit that moment with CodeMouse at about 150,000 lines of JavaScript, and the migration to TypeScript took eight months.
Here's what we got right, what we got wrong, and the framework we'd use if we were doing it again.
The Business Case First
TypeScript migrations require sustained investment across many sprints, and that investment will be challenged every time a product deadline approaches and the migration work looks optional compared to feature work. Making the migration succeed requires building the business case before the first file is renamed — and the business case needs to speak in terms that non-engineers understand: reduced bug rate in production, faster onboarding for new engineers, reduced time spent investigating type-related production incidents.
We tracked our pre-migration production bug rate by type category. Type errors — passing the wrong kind of value to a function, accessing a property that might be undefined, misunderstanding what a function returns — accounted for roughly 35% of all production bugs. This number became the anchor for every conversation about migration priority. TypeScript doesn't eliminate all bugs, but it eliminates this entire category reliably.
The Incremental Strategy: Never Break the Build
The migration strategy that fails is the one that creates a separate TypeScript branch and tries to migrate everything before merging. The strategy that works is the one that migrates incrementally, file by file, while the main branch remains shippable at every commit.
The practical implementation: enable TypeScript with allowJs: true and strict: false. Add TypeScript incrementally to new files and to files being changed for other reasons. Never allocate sprint capacity exclusively to migration — migrate as a side effect of normal feature work. Enable stricter TypeScript settings module by module as the codebase matures.
This approach is slower than a big-bang migration but produces better outcomes: the team builds TypeScript fluency gradually, the migration doesn't block shipping, and the TypeScript files that are written alongside ongoing feature work tend to be better typed than files migrated mechanically during a dedicated migration sprint.
The any Tax
The fastest path through a TypeScript migration is to type everything as any and call it done. This is the worst possible outcome — a TypeScript codebase with pervasive any usage provides almost none of the safety guarantees that motivated the migration and all of the ceremony of TypeScript without its benefits.
Treat any as a temporary loan, not a permanent solution. Track any usage in your codebase. Set a policy: no new any usages without a comment explaining why it's necessary and what condition would allow it to be removed. Reduce existing any usages by 20% per quarter until the codebase is clean. The teams that enforce this policy have dramatically better TypeScript codebases than the teams that treat any as a migration tool.
Code Review Changes During Migration
TypeScript migration changes what code review looks for. Reviewers need to develop fluency in TypeScript's type system to evaluate whether types are accurate and meaningful rather than technically valid but semantically loose. A function typed as (data: any) => any satisfies the TypeScript compiler but tells reviewers nothing. A function typed as (data: ReviewPayload) => ReviewResult documents the contract explicitly.
Build TypeScript-aware patterns into your review culture: expect type exports for all public interfaces, require return types on all exported functions, flag overly broad types as candidates for refinement. The code review culture you build during migration sets the quality standard for the TypeScript codebase you'll maintain for years.
Try CodeMouse on your next PR
Free AI code review on every pull request. Bring your own API key — no subscription needed.
Install on GitHub — Free