Mark Pollmann's blog

Migrating a React Codebase to TypeScript

· Mark Pollmann

Note: This post was originally published in July 2018 and has been updated for accuracy and comprehensiveness (you know, hooks and stuff).

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

create-react-app comes with TypeScript support as of version 2.1. Converting is quite straight-forward now:

Install Dependencies

$ yarn add typescript @types/node @types/react @types/react-dom @types/jest

Add a TypeScript Config

Add a tsconfig.json file at your project root level with the following content:

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react"
  },
  "include": [
    "src"
  ]
}

Change the file endings

Change all file endings from .js 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.

Feel free to set strict to false in your tsconfig.json for now and sprinkle any type annotations over your codebase where TypeScript complains. No need to fix everything in one go. Adding types can be done in small, self-contained pull-requests later on.

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

Note: This works out of the box now.

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 MyHeaderProps {
  isDragging: any;
}

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

See the emotion docs on TypeScript.

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!

Typing Your Codebase

Functional Components

I type my components with React.FC even though it has some downsides.

type MyBananaProps = {...}

const MyComponent: React.FC<MyBananaProps> = props => {}

Note that you don’t need to type the props argument again in your arrow function.

The alternative is to just type your props:

type MyBananaProps = {...}

const MyComponent = (props: MyBananaProps) => {}

There’s actually quite the mess with regard to typing React components. There’s React.ReactElement, JSX.Element, React.ReactNode, React.FunctionComponent, etc… But unless you run into problems keep it simple.

Give me something to type any generic render output

React.ReactNode. It’s not very type-safe but it gets the job done when you want to allow basically everything. The internal typings look like this:

type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;

// where ReactChild is 
type ReactChild = ReactElement | ReactText;

// and ReactText
type ReactText = string | number;

Typing Events

Events can be tricky to type correctly but it’s worth it to catch potential bugs. In general, you want to import the most specific event from React and use its generics to tell TS on what HTML element this event will be triggered:

import React, { MouseEvent, KeyboardEvent } from "react";

const MyComponent = () => {
  const handleClick = (event: MouseEvent<HTMLButtonElement>) => {};

  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {};

  return (
    <>
      <button onClick={handleClick}>Click me</button>
      <input onKeyDown={handleKeyDown} />
    </>
  );
};

Here is a quick overview of event types I found:

BaseSyntheticEvent
SyntheticEvent
ClipboardEvent
CompositionEvent
DragEvent
PointerEvent
FocusEvent
FormEvent
InvalidEvent
ChangeEvent
KeyboardEvent
MouseEvent
TouchEvent
UIEvent
WheelEvent
AnimationEvent
TransitionEvent

Typing Hooks

Most built-in hooks (useReducer, useContext, useEffect, useCallback, useMemo) should be straight-forward to type so I will focus on useState and useRef.

useState

Simple types get inferred:

// ❌ Unnecessary
const [counter, setCounter] = useState<number>(0);

// ✅ `number` is inferred
const [counter, setCounter] = useState(0);

For more complex types it’s quite straight-forward:

type MyCustomState = {counter: number, running: false}

const [myCustomState, setMyCustomState] = useState<MyCustomState>(yourInitialState)

// or 

const [myCustomState, setMyCustomState] = useState<MyCustomState|undefined>(undefined)

useRef

This hook can be troublesome as many developers like to take a big step around DOM typings. But it’s actually not so bad:

const MyComponent = () => {
  const myButtonRef = useRef<HTMLButtonElement>(null)
  const myDivRef = useRef<HTMLDivElement>(null)
  const myCircleRef = useRef<SVGCircleElement>(nul);

  if (myButtonRef.current) {
    myButtonRef.current.focus();
  }

  return (
    <>
    <button ref={myButtonRef} />
    <div ref={myDivRef} />
    <svg>
      <circle ref={myCircleRef}
    </svg>
    </>
  )
}

You don’t need to add null to the generic type (e.g. useRef<HTMLButtonElement|null>) as the React typings have overloads for this:

// from @types/react/index.d.ts
function useRef<T>(initialValue: T|null): RefObject<T>;

But you still need the check of the current property of your ref (see line 6 above), as current gets initialized with undefined before it grabs its ref.

Where to learn more