Migrating to Typescript: Keeping it smooth & steady
TJF
Posted on September 22, 2020
In the process of transitioning two mature frontend codebases (totalling ~5k files / 300k lines) from Javascript to Typescript, I learned a bit about the process that might be helpful to anyone considering this change in their stack. Much of this advice is not really specific to Typescript, and could possibly be generalized to any language or framework migration — but I’ll stick to what I know.
1. Write all new code in Typescript — sometimes
Don’t create extra work for your team down the road by pushing more code to be migrated later — as soon as Typescript has been introduced into your stack, every pull request going forward should be written in TS. But how strict should this be? It’s easy enough to write new files and components in TS, but what about changes to existing files? If a pull request changes just one line in a file, should the whole file be converted?
Requiring devs to migrate every changed file can be a morale and productivity killer. Even the smallest bug fixes become chores, and PRs for new features are impossible to review as the diff often interprets migrated files as being new. On the other hand, if the migration isn’t required, the work might never get done. This is particularly true for older files that aren’t edited often. Find the balance that makes sense for your team and keeps the migration moving forward.
2. Convert common files and generic components first
You’ll get the most immediate benefit from Typescript’s features by targeting the files most likely to be imported in any new feature work. If these shared components aren’t converted, you’re building up tech debt in all of your new TS files. Get ahead of this and enjoy the autocomplete and instant errors on all your new files.
Use the most accurate type available for all the properties of the API on these core components. It can be challenging to find the exact type for functions, callbacks, and events (especially for React events, DOM properties, or third-party dependencies), but it will save you trouble downstream in your consumers. Going slowly to get the core components right will save you time overall.
3. Communicate with your team about upcoming migrations
Migrations can sometimes create huge diffs that lead to nightmarish merge conflicts if multiple devs are working in the same file. Don’t set yourself up for pointless hours of frustrating and buggy conflict resolutions. Check in with your team before starting a migration. If there’s significant activity in that area of the code, consider postponing the work or basing your branch from theirs.
4. Resist the urge to refactor
When migrating any large codebase, you’ll inevitably find yourself opening ancient files with terrible debt. Gross, look at all these deprecated patterns and inefficiencies. Oh, you could totally write this in a third of the lines. No one’s using this function any more, right? Snip-snip.
It’s hubris. Don’t do it. You’re going to create regressions. Be good to yourself and your team. Don’t stress QA out.
5. Avoid maintaining parallel versions of the same component
Sometimes, migrating a complex or highly-used component is just too fraught to risk an in-place conversion — in such situations, the only choice might be to duplicate, convert, and gradually swap out the old for the new throughout your codebase. But as long as both versions exist, there will be confusion about which one to use; the old one will get imported by habit or copy-paste; bug fixes and enhancements will need to be applied to both; behavior may even drift over time.
Minimize the amount of time spent in this situation — when adding a duplicate TS component, prioritize this area of migration. Name your files and components clearly to avoid confusion when importing.
6. Account for migrations in estimations and planning
When providing time or point estimates for future work, add another 20% if you plan on migrating the code first. Plan out your migrations; if you know that major work is upcoming in one area, get the migration done early. Don’t leave it as an invisible or unexpected cost.
7. Leave a comment whenever you use ts-ignore
or any
Some of your third-party dependencies are going to give you incorrect type definitions that leave you scratching your head for days. An obscure corner case with generic tuples will send you down a Stack Overflow wormhole for 5 hours. The greatest good is to keep moving forward and leave good comments whenever you’re forced into a hack.
8. Don’t let it linger
One of the possible migration routes for Typescript is to progressively increase its strictness; turning off these rules can be helpful while getting started, but delaying this for long will hold your team back from the full benefits of the language. It can be a high cost initially as you get the necessary dependencies migrated so even a single component can be fully typed, but it’s better than facing a massive diff on the horizon once you do enable it.
There will be boom and bust periods in the speed of change, but mid-migration tech debt is exhausting to deal with. You can never remember which component has been converted already or not. You still can’t trust the IDE to catch some of the biggest mistakes in your code. Why did we even start this stupid transition in the first place?
But the virtues of a strongly typed language grow exponentially as more of the codebase is converted. The first time you refactor a core component and the compiler immediately tells you down to the line and column where the fixes are needed, it’s all worth it. Most other languages have had this experience for decades, of course, but not Javascript.
Good luck! It’s a lot of work, but it will eventually pay off.
Posted on September 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.