Navigate back to the homepage
📚 Books

Migrating a React Codebase to TypeScript

Mark Pollmann
May 5th, 2020 · 3 min read

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

1$ 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:

1{
2 "compilerOptions": {
3 "target": "es5",
4 "lib": [
5 "dom",
6 "dom.iterable",
7 "esnext"
8 ],
9 "allowJs": true,
10 "skipLibCheck": true,
11 "esModuleInterop": true,
12 "allowSyntheticDefaultImports": true,
13 "strict": true,
14 "forceConsistentCasingInFileNames": true,
15 "module": "esnext",
16 "moduleResolution": "node",
17 "resolveJsonModule": true,
18 "isolatedModules": true,
19 "noEmit": true,
20 "jsx": "react"
21 },
22 "include": [
23 "src"
24 ]
25}

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):

1declare module "*.png" {
2 const value: string;
3 export default value;
4 }

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:

1const Header = styled.div`
2 color: BlanchedAlmond;
3`
4// [...]
5
6<Header
7 isDragging={snapshot.isDragging}
8/>
9// [...]

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:

1interface MyHeaderProps {
2 isDragging: any;
3}
4
5const Header =
6 styled("div") <
7 MyHeaderProps >
8 `
9 padding: 0px 0 0 8px;
10 margin: 5px;
11 display: flex;
12 justify-content: space-between;
13`;

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.

1type MyBananaProps = {...}
2
3const 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:

1type MyBananaProps = {...}
2
3const 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:

1type ReactNode = ReactChild | ReactFragment | ReactPortal | boolean | null | undefined;
2
3// where ReactChild is
4type ReactChild = ReactElement | ReactText;
5
6// and ReactText
7type 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:

1import React, { MouseEvent, KeyboardEvent } from "react";
2
3const MyComponent = () => {
4 const handleClick = (event: MouseEvent<HTMLButtonElement>) => {};
5
6 const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {};
7
8 return (
9 <>
10 <button onClick={handleClick}>Click me</button>
11 <input onKeyDown={handleKeyDown} />
12 </>
13 );
14};

Here is a quick overview of event types I found:

1BaseSyntheticEvent
2SyntheticEvent
3ClipboardEvent
4CompositionEvent
5DragEvent
6PointerEvent
7FocusEvent
8FormEvent
9InvalidEvent
10ChangeEvent
11KeyboardEvent
12MouseEvent
13TouchEvent
14UIEvent
15WheelEvent
16AnimationEvent
17TransitionEvent

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:

1// ❌ Unnecessary
2const [counter, setCounter] = useState<number>(0);
3
4// ✅ `number` is inferred
5const [counter, setCounter] = useState(0);

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

1type MyCustomState = {counter: number, running: false}
2
3const [myCustomState, setMyCustomState] = useState<MyCustomState>(yourInitialState)
4
5// or
6
7const [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:

1const MyComponent = () => {
2 const myButtonRef = useRef<HTMLButtonElement>(null)
3 const myDivRef = useRef<HTMLDivElement>(null)
4 const myCircleRef = useRef<SVGCircleElement>(nul);
5
6 if (myButtonRef.current) {
7 myButtonRef.current.focus();
8 }
9
10 return (
11 <>
12 <button ref={myButtonRef} />
13 <div ref={myDivRef} />
14 <svg>
15 <circle ref={myCircleRef}
16 </svg>
17 </>
18 )
19}

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

1// from @types/react/index.d.ts
2function 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

More articles from Mark Pollmann

TypeScript 3 - What you might have missed

const assertions, the unknown type, optional chaining and more.

January 9th, 2020 · 1 min read

Career Options for Developers

Management, consulting, building a company? What to do when you're stuck in a rut.

December 7th, 2019 · 5 min read
© 2017–2020 Mark Pollmann
Link to $https://twitter.com/MarkPollmannLink to $https://github.com/MarkPollmannLink to $https://www.linkedin.com/in/mark-pollmann-961446132/