TypeScript: The Pragmatic Migration
Migrating a 50,000-line JavaScript codebase to TypeScript without stopping feature work. What worked, what didn't, and what I'd skip.
The Starting Point
The codebase was a 50,000-line React application that had been built over two years by a team that grew from two to eight developers. JavaScript throughout, no type checking, and the kind of subtle bugs that untyped codebases accumulate — a function that expected a string but sometimes received a number, an API response shape that drifted from what the frontend assumed, a refactoring that changed a function signature without updating all the call sites. The bugs weren't catastrophic, but they were constant, and each one required debugging time that added up to a meaningful drag on the team's velocity. The decision to migrate to TypeScript was driven by the engineering team, not by management, because we were the ones living with the bugs every day.
The challenge was that we couldn't stop feature work for a migration. The business had a roadmap with committed deadlines, and "we're rewriting our codebase" wasn't an acceptable reason to miss them. That constraint shaped the entire migration strategy: it had to be incremental, it had to be invisible to the business, and it had to produce value at every step rather than only at the end. A big-bang migration — convert everything, then turn on type checking — was off the table because it would take weeks and produce nothing shippable during that time. We needed a strategy where every week's work was independently shippable and independently valuable.
The Strategy
The strategy we settled on was allowJS — configure the TypeScript compiler to accept both `.js` and `.ts` files in the same project, and migrate files one at a time. We started with the leaf files — utility functions and small components with no internal dependencies — because they could be migrated without needing to migrate anything else first. Each migration was a small PR: rename the file, add types, fix the errors the compiler found, merge. The PRs were small enough to review carefully and fast enough that we could do several per week without disrupting feature work. Over six months, we migrated the entire codebase file by file, and by the end, the application was fully TypeScript with strict mode enabled.
The most valuable part of the migration wasn't the end state — it was the bugs the compiler found during the process. Every file we migrated surfaced issues that had been living in the codebase undetected: functions that received unexpected types, API responses that didn't match the frontend's assumptions, imports that referenced modules incorrectly. The compiler found dozens of bugs that would have eventually surfaced in production, and it found them at migration time — when we had context on the code and could fix them properly — rather than at runtime, when we'd be debugging under pressure. That proactive bug-finding was the immediate payoff of the migration, and it was visible to the business as a reduction in production incidents even before the migration was complete.
What I'd Do Differently
In hindsight, the one thing I'd do differently is to be more aggressive about typing the API boundary early. We migrated frontend files first and left the API client for later, which meant that for the first few months, the types we wrote for API responses were based on our assumptions rather than on the actual API contract. When we finally typed the API client, we discovered that several of our assumptions were wrong, and we had to go back and fix types across the codebase. If we'd started with the API boundary — even just generating types from the backend's OpenAPI spec — we'd have caught those mismatches earlier and avoided the rework.
The other lesson is about strictness. We enabled strict mode from the beginning, which meant every migrated file had to be fully typed with no `any` escape hatches. That was painful in the short term — some files took much longer to migrate because we had to figure out the right types for complex patterns — but it paid off in the long term because it prevented the "gradually typed" codebase where some files are properly typed and others are `any`-everywhere. A codebase that's partially typed with strict mode is dramatically more valuable than a codebase that's fully "typed" with `any` everywhere, because the strict files catch real bugs while the `any` files provide no value. Enabling strict mode from day one, even though it was painful, was the right call.
