In summer 2021, we migrated our codebase from Flow to TypeScript. There were many reasons for doing this, but the biggest ones were TypeScript’s community, project activeness, and maintainability.
Additionally, Flow is no longer an actual open-source project, and it’s hard to depend on a project that deprioritizes bug fixes that aren’t a priority inside Facebook’s codebase.
This blog post will discuss various aspects of the migration, including the planning, the implementation, and how we handled errors.
This project was handled by two members of the team, with the remaining members continuing with their normal day-to-day work.
The planning was a more significant challenge than the actual conversion. Since we had to convert a big codebase, it had to be done in a way that affected the developers working on the codebase the least. I’m using the term “least” because there was no way we could have avoided it entirely.
One of the biggest mistakes we could have made in this phase would have been creating massive pull requests that are hard to review — so we were committed to making small PRs. Additionally, we wanted to make sure every PR that was merged to master was stable. This meant two things:
We were able to release the stable branch even when the migration task was going on.
Even when the stable branch had a mix of Flow and TypeScript files, everything worked as expected.
Our migration task went forward in phases, which I’ll outline next.
We decided to migrate the e2e tests first, because they didn’t affect our SDK’s functionality, which made them the safest tests to migrate. Additionally, we rely heavily on our tests setup to tell us whether or not something is broken, which means all other migration PRs that followed would use them as a basis for determining if everything was correct. The team kept working on the codebase, and whenever there were any conflicts, those were easier to resolve, as our e2e test files are small and focus on running specific behavior tests.
Unit tests were next in line. While we worked on converting the unit tests, we had to set many types to
any, since the types from the SDK (written in Flow) weren’t available. We added a TODO comment in every place where we made this change so that we could come back later and fix them.
Once we migrated the tests, we started working on migrating the SDK code. The migration went on for a week, and the whole web codebase was in DND (Do Not Disturb) mode for that entire week. Since we were working on the most critical part of our business offering, we wanted to make sure nothing unexpected happened. Additionally, it was pretty easy for developers to get conflicts in this phase, and we wanted to avoid that if possible. Luckily, we were also working on revamping our documentation while the two of us worked on this migration, so everyone else shifted their focus to writing the guides.
We used flow-to-ts to convert the codebase from Flow to TypeScript. After running the script on a single folder, we looked into the code to spot any unexpected changes, and we were surprised to see the accuracy of flow-to-ts. Most of the time, the only manual changes we needed to make were to declare some global variables in TypeScript.
We made a strict rule that no actual code would be changed, even if it was required to fix a type error after conversion to TypeScript. Instead, the only things changing in the file should be the types. Whenever we encountered such a situation, we added a TODO comment and came back to it later, as mentioned earlier.
Most of the time, if there were any errors while migrating, we could solve them, but sometimes it wasn’t possible to do so without changing the actual code. In those cases, we used
// @ts-expect-error, which allowed us to come back to issues later and then fix them by adding a comment above the code in question. If you’re curious about why we used
ts-expect-error instead of
ts-ignore, please read this article. The good thing is that we’ve already gotten rid of more than half of these error suppression comments, and we’re fast approaching the stage where there will be no ignored errors in our codebase.
We’ve experienced many improvements since migrating, but the highlights have been faster static type checking and out-of-the-box support for TypeScript declarations. Additionally, if we’re stuck with something, we know we have a huge community to bank on. And finally, we no longer have to wait for months for a fix, as TypeScript has periodic release cycles, which keeps things predictable so that we can prepare accordingly.