Migrating an Ejected Create React App to TypeScript

I love Create React App, and I also love TypeScript. That means I really love react-scripts-ts, which is a great way to spin up a new SPA for experimental purposes.

Unfortunately, there’s no trivial way to take an existing Create React App (CRA) and make it use react-scripts-ts, even if the CRA hasn’t been ejected. The bright side is that it isn’t too difficult to do the migration manually. @trichards57 posted a great synopsis in this GitHub issue comment, which @galvanu later expanded on in this StackOverflow answer. While I haven’t tried either of these methods personally, they seem like simple, yet thorough, guides to migrating.

But what if you’ve already ejected a CRA and want to start using TypeScript? There are a number of ways to do this. You could use TypeScript’s guide to React and webpack, but this requires working through all of the webpack configuration that CRA has already set up.

Instead, I recommend using the same strategy as @trichards57 and @galvanu: Create a separate CRA with react-scripts-ts, and then slowly incorporate it into your existing code base. It’s slightly more complicated once the app is ejected, but I think it’s the most straightforward solution.

This guide is based on my experiences with migrating a single ejected CRA to TypeScript. Your mileage will almost definitely vary! Note that your existing ejected Create React App will be referred to as “your app,” while the app created with react-scripts-ts will be the “TCRA” (for TypeScript Create React App).

1. Get Started

Create a new Create React App with the same name as your app, but use react-scripts-ts:
create-react-app my-app --scripts-version=react-scripts-ts.

Then eject the app without making any changes:
cd my-app
npm eject

That’s all you have to do to the TCRA!

2. Merge the config/ Directories

If you haven’t made any changes to your app’s config/ directory since ejecting, go ahead and replace the entire directory with the one built by the ejected TCRAs. If you have made changes (or you aren’t sure), do a git diff between the two directories, something like this:
git diff ./js/my-app/config/ ./ts/my-app/config/

Then manually merge the files so that you keep both the TypeScript-related code and any custom configuration. Keep in mind that the TCRA includes more files under config/ than your app did when it was first ejected.

3. Add the TypeScript-Specific Files

The TCRA created a few files in the root directory which you need to copy over to your app:
images.d.ts
tsconfig.json
tsconfig.prod.json
tsconfig.test.json
tslint.json

I also recommend removing the contents of tslint.json (except for an empty {} object) until TypeScript has been fully integrated into your app. Worrying about lint errors while trying to insert types everywhere can be bothersome.

In addition, copy the TCRA’s src/index.tsx into your app’s src/ directory. This will allow the app to compile before the other src/ modules are converted.

4. Merge package.json

Merge your package.json with the TCRA’s version. This may be painful, but remember that git diff’s output options are your friends here. Also, don’t forget to merge changes outside of the “dependencies” and “devDependencies” sections (e.g. “globals” and “jest”).

5. Run a Smoke Test

Run npm start. If you’ve merged everything correctly, there should be no compilation errors. Now would be the perfect time to git commit, because the final step may get complicated.

6. Migrate the src/ Directory

Migrate everything in the src/ directory. The amount of time this takes will depend heavily on the size of your application and your experience with TypeScript. For example, my application was small (probably about a thousand lines inside src/), but TypeScript was also very new to me. This part took at least a few hours. Here are some tips:

  • Start by deleting index.tsx and changing the config back to index.js.
  • Work from the “leaves” of the application, or modules that don’t depend on other files in your project. Examples might include presentational components and/or Redux action creators.
  • Work through one file at a time, in its entirety. Change the file extension to .tsx, and don’t touch another file until all of the compilation errors have been handled.
  • If you’re getting blocked by a specific TypeScript error, don’t be afraid to cast an expression as any. My strategy was to add an easily-searchable comment (“// TODO-TS”) near any hack-y solutions. That way, I could go back to solve any hard problems after the tedious work was complete.
  • If you use lots of third-party libraries, consider using this tool. I haven’t personally used it, but it might be a good alternative to running npm install every thirty seconds.
  • Avoid the strong urge to refactor (for now). TypeScript will inevitably expose refactoring opportunities, and you should absolutely scratch that itch later on. But for now, stay focused on getting your app to compile with all of its newly-introduced types.

7. Finish Up

Your .js(x) files have all been converted to .ts(x). Your app is compiling and running, and the tests are green. If you have CI set up, the tests in there are green, as well.

Now it’s time to tidy things up by acting on those TODO-TS comments and confirming that any non-component files (e.g. tests, mocks, redux modules, service workers, storybook modules) are using TypeScript. You should also repopulate the tslint.json file, tailoring it to your project’s needs.

Voilà! Your ejected CRA is now using TypeScript, and an adventure of refactoring is probably on the horizon. That wasn’t so bad, was it?

Once again, these steps are based on my experience with migrating one ejected CRA. There are probably many scenarios that this guide doesn’t cover, and I hope you leave a comment about them below. In the meantime, have some type-safe fun out there!