Migrating a React Codebase to TypeScript

9 minute read

Introduction

More and more React developers are starting to appreciate the type safety TypeScript allows when working with React. Libraries like Formik, react-apollo or anything from the Prisma people have been leveraging it for years.

Here are the steps to you need to take to get that warm fuzzy feeling when your code compiles.

Migrate create-react-app

If your project was built with create-react-app (and why shouldn’t it be, it’s amazing) you might think you have to create a new project from scratch before migrating. Not so! We will be mostly following the steps laid out by user galvan on StackOverflow.

Adapt react-scripts

In your package.json change all appearances of react-scripts to react-scripts-ts. The latter is the semi-official TypeScript fork of Will Monk of create-react-app. Your scripts part of the package.json should now look like this:

  "scripts": {
    "start": "react-scripts-ts start",
    "build": "react-scripts-ts build",
    "test": "react-scripts-ts test --env=jsdom --verbose",
    "eject": "react-scripts-ts eject"
  }

Install the script with your favorite package manager: yarn add react-scripts-ts or npm install react-scripts-ts. A rm -rf node_modules && yarn cache clean && yarn might not hurt if you ever get some weird errors in this process.

TypeScript config files

You need a couple of TypeScript-specific config files. The easiest way to get those is create a temporary TypeScript create-react-app project, copy the files to your real project and then delete the temporary project.

Run create-react-app --scripts-version=react-scripts-ts somewhere outside of your real project. This can take a couple of minutes. When it’s done locate the following files and copy them to the root folder of your real project: tsconfig.json, tsconfig.test.json, tsconfig.dev.json, tsconfig.prod.json and tslint.json. You can delete the temporary project now.

Install the must-have types

yarn add @types/node @types/react @types/react-dom or npm install @types/node @types/react @types/react-dom.

Change the file endings

Change the file endings to .tsx. It’s a must for your index.js but you can do it for all your JavaScript files right now if you want.

Adapt your tsconfig.json

Allow synthetic default imports, turn off NoImplicitAny checks (temporarily) and add a newer lib if you want to use the newest EcmaScript features. This is what the compilerOptions part of the tsconfig.json could look like:

"compilerOptions": {
    "baseUrl": ".",
    "outDir": "build/dist",
    "module": "esnext",
    "target": "es5",
    "lib": ["es6", "dom", "es2018"],
    "sourceMap": true,
    "allowJs": true,
    "jsx": "react",
    "moduleResolution": "node",
    "rootDir": "src",
    "forceConsistentCasingInFileNames": true,
    "noImplicitReturns": true,
    "noImplicitThis": true,
    "noImplicitAny": false, //temporal
    "strictNullChecks": false,
    "suppressImplicitAnyIndexErrors": true,
    "noUnusedLocals": true,
    "allowSyntheticDefaultImports": true
  },

While you’re at it, you might want to add the following rules to tslint.json at least temporarily:

"rules": {
    "no-console": false, // allow some console statements for debugging
    "member-access": false, // start without having to write public, private modifiers
    "jsx-no-lambda": false, // lambdas in render are bad for performance but this can be annoying during prototyping
    "jsx-boolean-value": false // allow someProp instead of someProp={true}
  }

Adapt your React imports

TypeScript and JavaScript modules seem not to be on the same wavelength regarding default imports. The easiest solution is to adapt your import React from 'react' statements to import * as React from 'react' and change the rest of your React code accordingly.

Import types for your libraries

The people over at DefinitelyTyped have ready-made types for almost all the libraries out there, so take advantage of it. Add them via yarn or npm (Example: get react-router-dom via yarn add @types/react-router-dom).

Troubleshooting

You will run into some problems on your way to the perfect TypeScript codebase but you will also learn a lot in the process. Here are some of the issues that came up for me:

Importing images

Your regular import logo from './logo.png' doesn’t work out of the box for TypeScript. This is actually webpack doing the work, not normal ESModules. Fortunately there is an easy fix to keep using this syntax. Add the following to any .d.ts file (or create one somewhere in your project, for example called index.d.ts):

declare module "*.png" {
    const value: string;
    export default value;
  }

TypeScript will not check for you if those images actually exist! If you declare a module like this you basically tell the compiler “Don’t worry, I got this!” and he trusts you. This will fail at runtime if the image-file is not where you expected it to be, so be careful.

CSS-In-JS unexpected extra props

TypeScript is not happy if you slap on unrecognized props to your custom component. To illustrate what I mean with this check out this code fragment:

const Header = styled.div`
  color: BlanchedAlmond;
`
// [...]

<Header         
    isDragging={snapshot.isDragging}
/>
// [...]

Here I’m using react-beautiful-dnd for drag-and-drop and the Header-div needs an additional prop. TypeScript doesn’t like it (for good reason: a <div> having a isDraggin attribute is unexpected).

The solution: My preferred CSS-in-JS library (emotion) allows the following:

interface IMyHeaderProps {
  isDragging: any
}

const Header = styled('div')<IMyHeaderProps>`
  padding: 0px 0 0 8px;
  margin: 5px;
  display: flex;
  justify-content: space-between;
`

See the emotion docs on TypeScript.

Bonus: use VS Code

Besides being an excellent mix of an editor and IDE, VisualStudio Code is closely developed with TypeScript (both are from MicroSoft). Definitely check it out if you haven’t!

Where to learn more



While you're here:

I'm available for work in Munich, Vienna or remotely. Shoot me an email.

Updated: