How To Incrementally Adopt TypeScript In A Large Codebase

Daniel Merrill
Async
Published in
5 min readJun 16, 2021

--

Adopting TypeScript in an established codebase is a big task that takes a long time, but it doesn’t have to be painful. Other companies have documented their conversion process (Sentry comes to mind), but the process I describe here is a little different—you could call it “lazy adoption”, because it doesn’t involve a single huge effort or any big scary pull request diffs. I’m also not here to convince you that you should adopt TypeScript—I’ll assume if you’re reading this you’ve already made that decision.

The project at hand is a React Native app that is ~75,000 lines of code across ~650 files. The conversion happened in three main stages:

  • Initial team buy-in
  • Convert JavaScript files to TypeScript
  • Fix all type errors and enforce type checking in CI

Team Buy-in

Having team buy-in is probably the most important part of this whole process. If the team agrees that adopting TypeScript will eventually make their lives easier, they’ll be motivated to make it happen. I recommend making it clear why TypeScript is being adopted beyond a vague promise of “fewer bugs”.

In our case, a typed data store made working with app data much faster, safer, and easier. It’s hard to over-emphasize the daily benefit of having IntelliSense fill in deeply-nested object properties as you type, or of auto imports that “just know” where that often-used utility function lives. The developer no longer has to hold as much information in their head about the data or codebase—they immediately see what’s available as they write their code.

The result of this mental offloading is the “fewer bugs” that is often promised—if the developer no longer needs to know ahead of time which properties may or may not exist, they won’t be able to introduce a bug by, for example, forgetting to null-check an object before accessing one of its properties.

File Conversion

Our team agreed that whenever a developer worked on a js file, they’d change it to a ts or tsx file. We weren’t overly-concerned with fixing type errors at this stage. The developer would often add some of the low-hanging types but leave the complicated stuff to be figured out later.

We immediately noticed that working in these partially-typed files was friendlier than in JS, which is a lesson in itself—you don’t need to reach 100% type coverage to benefit from it—partial type coverage is better than no type coverage.

There are tools that promise to make this initial conversion easier (ts-migrate), but we didn’t use any of them. ts-migrate in particular doesn’t fit with the plan I’m describing—we depend on the type errors that will inevitably arise and we don’t want errors hidden behind //ts-expect-error as ts-migrate does (It’s possible this behavior can be configured in ts-migrate, I didn’t investigate).

By the end of this process all of our files were ts or tsx files and we had about 800 type errors to deal with. That may sound like a lot, but we chipped away at them as described in the next step and reduced that number to 0 over a few months.

Type Checking

This is the step where we made a concerted effort to tackle TypeScript issues as a part of our sprint goal. We created several tickets with titles like “Reduce TypeScript errors to 700”, then 600, 500, etc, and allocated them to future sprints. Of course, you can bite off as large or as small of a chunk as you feel comfortable with.

We also created some extra tooling to make this step friendlier. We wanted to be able to enforce the above thresholds in CI, but type checking is typically an “all or nothing” deal—simply running yarn tsc as a GitHub action would fail the entire workflow on any type error, so that wasn’t an option. However, we wanted to have some guardrails to prevent developers from checking in more type errors. As a stretch goal, we wanted pull request reviewers to be able to call out type errors before they were checked in, viewable within the pull request diff.

I found a GitHub action that identifies type errors and posts them to the pull request as annotations.: https://github.com/andoshin11/typescript-error-reporter-action.

Type errors posted as pull request annotations

This looked promising, but there were a few issues with it—most glaring, it threw an obscure error and didn’t actually run. But also, it is still an “all or nothing” check that fails the workflow on any type errors. Lastly, the GitHub action API is limited to posting 10 annotations, so the reported errors were severely truncated, and not guaranteed to include errors resulting from the work in the open pull request.

I decided to make the necessary changes myself. After cobbling together code from various other GitHub actions into a fork-of-a-fork of the above package, I had what I needed: an action that lets us set a maximum “error threshold” which fails the workflow when exceeded, and that posts all type errors as pull request annotations.

After ramping down our threshold over several sprints, a typical run looked like this:

Workflow step still passes if error threshold is above the number of errors found

Until…

And we were done! You can use this action in your own workflow: https://github.com/computerjazz/typescript-error-reporter-action

Conclusion

I won’t sugarcoat it — it took over a year to end up with a fully converted and typed codebase. However, the work never dominated our sprint goals and felt organic, trickling in bits and pieces as developers completed their other projects.

At the end of the process, the project manager was happy because we were still able to complete our feature work alongside the TypeScript conversion, and the engineers were happy to have a typed codebase.

--

--