A Bird’s Eye View of Migrating a React App to TypeScript

Heidi Kasemir
Udacity Eng & Data
Published in
7 min readJan 19, 2021

--

At Udacity, we have been using React in production for 5+ years. Considering how far React has come in that time, it’s no surprise that we are left with a considerable amount of legacy React code. However, it’s important to us to keep everything up to current best standards, so when it came time to decide on the highest-impact tech debt to tackle, TypeScript popped up front and center.

Certain members of the team (including me) weren’t convinced that converting to TypeScript would be that beneficial. Thinking about the thousands of lines of code spread across hundreds of files between components, services, views, reducers, selectors, helpers, and tests, it seemed that it wouldn’t be worth the lift. Could we get the same benefits by simply providing better test coverage?

After attending conferences and reading blog posts where TypeScript was the belle of the ball, we decided that it must be worth taking into serious consideration. Things like confidence in refactoring, developer ease with features like auto-complete, and integrations with GraphQL to auto-generate types based on our internal API schemas sounded like benefits worthy of the elbow grease. The decision to test it out as a proof-of-concept quickly developed into a project that changed how I look at my own and other peoples’ code, and now I wouldn’t go back.

Getting an app converted from good old JavaScript to TypeScript is no small feat, and I learned a lot of things along the way. This post is not intended to convince you to make the switch, nor is it a guide to writing your tsconfig — there are already a lot of resources out there that extol the virtues of static typing and show you how to get your compiler up and running. Rather, what I hope to share are some lessons I learned during the 8-month migration that took my app from zero to fully typed, that might just make your road a little easier if you’re planning to get started. Think of this as less of a step-by-step walk-through, and more of an overview of some of the traps that make migrating hard. Some of these are holes I had to dig myself out of, so save yourself the trouble, and read on!

Me and My Front-end Stack

Before we get started, I wanted to give a quick intro to me and my app, so that you can identify similarities and differences between what I’ll be talking about and your unique situation.

I started this process with just a little experience with TypeScript — I knew what it was at a basic level. I could write a type, but I didn’t really know about things like generics. The app that I work on is a dashboard built with React + Redux with a mix of GraphQL and REST API endpoints. It uses Reselect to create memoized selectors from the Redux store, and uses React-Router to show views based on URL state. As of the time of writing, it’s composed of over 100,000 lines of code. At the start of this project, it used mostly class-based components. By the end, I was fully drinking the hooks Kool-Aid, but we’ll get to that in a second.

Lesson #0— Start Small but Strict

When you first get your tsconfig setup and you’re deciding on where to start, find a relatively small file with few or no imports. A helper file may be perfect for this. On your first change, you basically only need to validate that your app will work with a .ts file in place of a .js file. Make it easy on yourself and go with something that is not likely to give you any headaches.

Also, be strict. Don’t allow yourself to take shortcuts here. It’s tempting to say “Oh, I’ll just type it as any for now, and I’ll circle back later once I figure this out.” Let’s be honest, you’re not going to circle back. You’re not going to update that tsconfig rule to a stricter value later and go back through every file it breaks. Set it up and do it right from the start.

Lesson #1 — Type the Things You Import First

Convert files with no or few imports before anything else. If you instead convert a file that is importing a local JavaScipt file, then it’s entirely possible that when you later get around to converting that imported file to TypeScript, there will be new type errors that you will have to track down in files you weren’t working on.

Try as much as possible to convert the files you import first, and only after you’re done with that, convert the files that import them. This means that good candidates to start with are services, helpers, and simple shared components.

Lesson #1.5 — Follow Along with an Online Course

Udacity is an online education platform, so while we don’t have a course specifically on TypeScript (yet!), we still recognize how empowered we are to learn skills using online resources.

If you already have lots of experience with TypeScript, feel free to skip this step. If you’re like I was, though, you may feel like you’re pointed in the right direction, but moving forward exposes gaps in your knowledge. Having already converted a few simple files, I started to understand the sort of issues I would come across. I had questions ready and recognized areas in my code where certain problems would crop up. Jumping into a course at this point in my journey was very useful because I learned advanced concepts, and was able to immediately apply what I just learned to my code.

Lesson #2 — Auto-Generate Types from GraphQL

I made the mistake of starting to type responses from our APIs without first looking into tools that provided types for me. If you can, use those from the beginning! For GraphQL, we now use graphql-codegen to inspect our server and provide up-to-date TypeScript types for all the data in our schema. It also inspects the queries we have in .gql files and provides types for the variables and the return types of each query and mutation. Suffice it to say, not having to maintain these by hand is definitely something you want, so get this set up early. Do this at the same time or before you convert your actions and API calls to TypeScript.

Lesson #3 — Get your Redux Store and Selectors in Shape

Going along with the idea of “type the things you import first,” the Redux store is likely to be a very important type to have defined early. This type can be inferred from your reducers, which take in your actions. They have to agree all down the line, so getting this solidly typed sets you up for success. The aforementioned courses on Egghead were very helpful in this step, as I learned about the ReturnType utility.

Using TypeScript in reducers helps you have confidence in the shape of your Redux state. If you refactor any action creators, the reducer will maintain the correct shape, or else your code won’t compile!

Once your reducers and store are all set up, your selectors are next. Using Reselect for this is a great way to standardize your selectors, memoizing them by default and allowing you to compose multiple selectors together to get what you need from the store. Selectors are also incredibly useful in your components to type the data coming in from your store when you use hooks.

Which brings me to…

Lesson #4 — Embrace Hooks

At this point, you’ve basically redone all the plumbing, but haven’t touched much of the JSX. Getting into converting the React Components was an exciting step for me, which quickly turned into pulling my hair out as I tried to keep things that were originally class-based, class-based. In a typical component, I would have to write a type for the mapStateToProps props, the mapDispatchToProps props, sometimes the mergeProps props, the component’s own props that it would receive from its parent or other HOC, as well as a State type if the component had any internal state.

Trying to use types in React Classes, especially with HOCs like Redux `connect` can be a real pain to get right.

You could do all this. Or, you could avoid repeated collisions of your face with your desk, and just use a functional component and hooks.

While this is not strictly a TypeScript recommendation, converting to a functional component with hooks significantly simplified the process of migration. On top of that, it exposed that the connect function was sometimes mapping certain state to props unnecessarily by including a prop that was never used in the component. By using hooks, unused variables are weeded out by the linter, and by having the selectors typed, defining a typed variable is as simple as

const nanodegrees = useSelector(nanodegreesSelector);

On average, I saw a reduction of 20% in lines of code for a component after converting from class-based JS to functional + hooks TS. Things became easier to read, unnecessary imports were eliminated, and types were safe, without having to spend effort defining more than the component’s own Props.

Lesson #5 — Keep at It

Honestly, it sometimes gets repetitive to change the file extension, then chase down all the compile errors. Perhaps I’m unique in this, but refactoring is a joy for me, and each file I converted was its own reward. If you are the type who needs more encouragement, a few strategies I used were:

  • Make sure that any new features are written in TypeScript.
  • If a file is touched for a different fix, consider converting that file.
  • If you don’t know what to convert next (once you’ve satisfied lesson #1), search for the keyword export in any .js or .jsx files and just go from the top.
  • Find your reward in seeing the percentage of TypeScript files ticking up with every PR.

It may take a long time to finish, so be prepared for that. I started in early November 2019, and didn’t finish until the end of July 2020. Of course, I was the sole developer and this migration was in conjunction with building out new features and the day-to-day bug reports that I would have to jump on. Your project may go faster! Either way, merging that last PR was immensely satisfying, and left me with a sense of confidence in the quality of my code.

Have you gone through this process on your own project? What challenges did you face that might help prepare other intrepid readers? Add to the comments with any tips that helped you along the way. Also, if you’re interested in working on a modern React stack at a company that is leading education for careers of the future, Udacity is hiring!

--

--

I am a Senior Software Engineer at Udacity. I geek out about React, front end testing, typescript, and rock climbing.