Migrating a Large Legacy React App to Typescript

Published on

Intro


Why

Where I work enforced types on our frontend code base is THE big missing piece in the stack of tools and techniques that help us have confidence in the products we ship. Unit tests with plain ol jest, integration tests with enzyme or react-testing-library, or end-to-end tests with Cypress are all great and these play a crucial role in that confidence, for sure, and the local linters like esLint and CI linters like Sonar do as well. But types, and specifically Typescript with a strict enough configuration, covers an angle, and grants a facet of confidence, that none of these other tools are especially well suited for.

Due to proximity to the code itself, type data alone would be very useful merely as documentation, but with typescript tooling you get lots of inline helpers as well as autocomplete suggestions in code editors, This helps prevent typing mistakes and other simple human error cases, however the advantages of enforced types go beyond just that! With the strictness of the compiler set at a certain threshold where the types have to have a degree of specificity and where the compiler blocks on type mismatches, the types actually become a test in themselves.

Types ARE tests, or to be more precise, enforced types are tests. If your code always passes through a compiler before it can run, or ship, and that compiler will not emit an output if the types are not correct, then your compiler is acting in the same capacity as an continuous integration application that refuses to build your software when its’ test suite doesn’t pass, meaning your types are functioning as tests.

Moreover, types ARE tests:

Of course, none of this is to say types supplant what is typically known as a test. Types don’t test logic, or functionality. They are however, an important piece of the confidence puzzle that we have been largely missing in our application up to now.

What

The “enforced types” part of the why statement above is key. Flow, putting type info in jsDoc comments, or just being more strict about React PropTypes, which we have used half-heatedly for a while now were all among the options considered to fill the gap of typing in this large react project.

We even looked at Dart and Flutter while working on the a recent mobile project, and honestly Dart + Flutter has plenty of appeal, the strong typing and good tooling probably go hand in hand for good reasons, however, Typescript was what we chose there, and experience on that project along with trends in the industry all show this is a very good choice.

Typescript’s compile time (rather than runtime) enforcement, superb developer tools, and widespread adoption (especially among tools and libraries we are already using) all work together to be able to make it an obviously good solution.

How

As with so many things, there is no “One True Way”™ to migrate a legacy project like ours to Typescript. There are two basic categories of approaches however - All at once, or piece by piece. The application in this case, and the number of developers that work on it, are both large enough that a stop everything all at once conversion is not very appealing.

More on migration approaches

The two big factors that affect how a piece by piece conversion actually happens are

  1. when to do a file re-name
  2. how/when to escalate the strictness the typescript compiler (transpiler?)

Of course it is really more of a spectrum, but again, it can be sort of broken down to two general approaches from a big picture level.

  1. Convert file by file in some systemic way - higher strictness to start
  1. Convert all files to .ts straight away - very low strictness to start

Tentative recommendation


Pros and cons for each approach, but for this situation I lean heavily towards option 1. “Convert file by file in some systemic way”

Option 2 kind of forces everyone to use TS, while option 1 makes it more opt-in. However, optional in a certain sense. For our team we said it wasn’t that “everyone who wants to should start using TS,” but more of a “on a squad by squad basis there is now the option to start using TS”. We want the people reviewing and interacting directly with TS code to be familiar enough with TS, so making sure each squad was on board together was important.

Option 2 also loses one of the key advantages of types in the early stages, it allows for very minimal typing and very little enforcement, and thus risks the team feeling like it is a fruitless effort - the effort to do TS isn’t to high at this stage but it also isn’t buying much confidence. Option 1 allows those who opt in to use Typescript to feel more of its full advantages from early on.

Either path would require some level of training, but Option 1 makes so the minimum is only need a basic overview in case they have to use or modify a component that has been converted. Option 2 would require much more alignment of training level as the each new strictness rule is implemented.

A good in depth learning resource available to our team was this in depth course on Udemy. A quicker overview is also available on YouTube

Our general path given that recommendation

Converting some larger core components will need many other types to be in place before it really makes much sense to do. The loose suggestion in the early stage is to work on moving simpler parts to TS first. We keep some of our more reusable in the elements directory, those were good candidates as were Models, and Util files, followed by things like reducers, selectors, actions etc. Another way to think of it is as starting from files that are imported a lot but that have few imports themselves. A potential next step after most of that is done would be requiring any new components to be in TS, and the conversion of existing components to TS before any significant new work is done on them.

With the settings we picked early in the migration the compiler was not so strict that it would completely prevent us from doing or shoddy partial conversions, however doing a half hearted conversion is very discouraged. If you are going to rename the file it is best to do as thorough job of adding types as you can for the entire contents of the file. This way anyone on the team can have a reasonable assumption that if the file is a TS file it has been converted and types for it are available when importing from elsewhere.

Many type definitions will live in the files they are used in, but not all. Details on where and how shared common type definitions will be located and organized should be a piece that settles sooner than later.

A short how to:

Here are a few steps involved with migrating a file to use Typescript. Some of them may not apply to every file and they may change slightly based on the file you’re working on. In general, you can follow these steps as a checklist for work that needs to be done on each file.