Now or Never: Migrating 300k LOC from Flow to TypeScript at Quizlet
At Quizlet we are constantly working on improving our developer experience. After two mostly happy years with Flow, we recently decided to move to TypeScript because of its community support, superior editor experience, and better type coverage.
And we decided to make the change all at once.
With the recent rise of TypeScript as the de-facto choice, we took a closer look at our use of Flow. We’d been noticing that the editor integration was at times painfully slow, and that core parts of the application, like the Redux state and component props, were not typed in any meaningful way, partly due to issues with the Flow typedefs for redux and reselect. We were also concerned about the future of Flow after a long period of silence from the maintainers.
As we started to explore TypeScript we were impressed with the editor support in VSCode, the more helpful error messages, and quality of the community support. Seeing other popular libraries and projects make the jump, and trying it ourselves, made it clear this was the right direction.
Let’s make it happen
Migrating almost 300,000 lines of code seemed a bit much to do manually, so we decided to convert the entire codebase all at once, using a babel plugin to make the syntax changes, and a script to rename files.
We considered an incremental migration, moving to TypeScript one file at a time. To preserve types across the language boundary during the migration, we could output definition files for both languages, and then generate conversions of those definition files to the other language. Ultimately we were concerned with the complexity of this approach, that engineers would have to know two different languages to work in the codebase, and that the migration could take a long time and generally add to the complexity of working in the codebase.
To prepare for the mass migration we first merged the TypeScript tooling, such as configuring Babel and setting up ESLint. We rehearsed running the migration over and over again, practicing getting CI green on the converted codebase quickly and reliably, and documenting the process in a runbook. Towards the end we were able to migrate the codebase and run all the manual steps to get CI green in about 12 minutes. This ensured that we would be able to minimize disruption to engineers, and that we would have time to address any issues if they came up.
Finally, on a day agreed upon, we placed a brief freeze on the codebase, went through the runbook, did some manual QA on the change, and merged it to production.
Of course, there were many TypeScript errors in the migrated codebase — over 6,000 in our case — and we decided to ignore the errors with // @ts-ignore comments to get CI green. After all, the type coverage with Flow was already not catching these errors. Ignoring errors allowed us to complete the migration all in one go rather than freezing the entire codebase while we fixed the errors.
We came up with a script to automatically insert // @ts-ignore comments into most of our code, but safely inserting ignores into JSX proved too complicated. We were left with around 1,400 errors in JSX, which everyone pitched in to fix manually over the following week. This provided a great opportunity for everyone to learn TypeScript together, getting familiar with the type system and best practices as we fixed the remaining errors.
Overall, the migration was quite successful. We’ve increased our type coverage from 66% to 86%, and now have full typing from Redux connectors down through the React component tree. The editor experience in VSCode is much snappier and more reliable, and we continue to find bugs in our code as we fix errors. And most importantly, engineers are starting to trust rather than fight the type system.
Of course, there were still things that we could have done better during the migration. After an internal retrospective we came up with areas where we could have done better:
- We should have a had more education, pairing, and working together on fixing errors in general. A TypesScript delegate on each team would have helped spread knowledge and drive the migration.
- We should have set clearer expectations for the effort required by product teams in advance, and provided an easy way for teams to know what work they were expected to do and what was remaining during the migration.
- We should have spent more time ensuring UI Components and other connector code was properly typed beforehand.
We’ll provide an update in a few months to check in on how it’s gone, and our progress towards getting to zero ignored errors. Meanwhile, if you’re interested in working on TypeScript at Quizlet, we are hiring!