Migrating a React Codebase to TypeScript
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
- TypeScript Cheatsheet
- The JSX part of the TypeScript Deep Dive book.