TypeScript and React - how to prevent bugs by typing props, state, and hooks

TypeScript is fantastic to use with React because it can help us catch a lot of bugs we wouldn't catch otherwise. It helps our team have good documentation and it makes React easier to use (over the long term).

But there were a lot of things I didn't understand in the beginning that I wish someone would have explained to me.

In this post, I'm going to walk you through the basics of using TypeScript and React, with code.

We're going to look at:

  • how to create a new TypeScript and React project (it's built-in now!)

  • how to type props for a class component

  • how to type state for a class component

  • how to type props for a functional component

  • how to type state for a functional component (with hooks!)

The key idea is that we want to define the "shape" of our props and state with TypeScript types -- and TypeScript will ensure that everything conforms to the right "shape" in our code.

Here's the interface we're going to be using for props and state throughout this post:

If you're not familiar with TypeScript interfaces yet, the above code says that any object of the CounterProps type must have a message key that is a string. We'll look at more code below.

Creating a TypeScript React Project#

So for getting started, thankfully, create-react-app has a typescript template that has everything setup for us!

You can create a new project like this:

It will create a bunch of files and the main addition is the tsconfig.js file which configures the TypeScript compiler.

You'll also notice that our files end in tsx instead of jsx.

Also you look at the package.json you'll see some extra typescript packages, too - but because they work out of the box, we'll ignore those for now.

The main thing we're going to be doing with TypeScript and React is defining components. So it makes sense to look at the typing of those components first.

As you may know, there are two ways you can define components:

  1. Class components or

  2. Function components

Let's look at both:

Typing Class Components with TypeScript#

To keep it simple, let's take the classic case of a component that shows a counter with a message.

A bare-minimum TypeScript class component looks like this:

So far, so good, but we're missing two key parts of React components:

  • props and

  • state

In React, we use props to pass down values into a component and state to manage state within a component.

By the way, if you're relatively new to React and this feels too advanced, you should checkout our free series 30 Days of React

If you think about it closely, props and state are both objects that need to have a type. Often we might have a component that has half-a-dozen props that could be passed in and we want to make sure that these props are valid keys AND valid values.

One of the great things about using TypeScript with React is that the compiler will enforce this for us.

So let's define an interface for the props to this component. We want to accept a message into this component as props like this:

If you think about it, what we're doing is passing this object into the component:

We can define an interface in TypeScript that describes this object. We'll do it like this:

Adding Props Type#

But how do we tell React that the CounterProps interface is the types for our component props?

We do this by using TypeScript generics on the React.Component type.

You can think of generics as parameters to a type. Generics let you make "configurable" types. It's easier if we look at the code:

Now let's say we try to call this component without passing a message prop like this:

... we get an error! Here's a screenshot from my VSCode:

There's a lot there, but the key point is that it's saying Property 'message is missing'

Okay, so we know we need to pass a message prop. Let's try passing an invalid message prop and see what happens:

Above, the number 123 is invalid because we defined our message to be a string (not a number).

Here's what I get in VSCode:

Again, a lot there but the key idea is that it tells me:

Type 'number' is not assignable to type 'string'

Pretty cool!

Typing state#

Okay so that deals with typing props, what about state? We deal with that much the same way, first we'll define an interface for the state:

Let's keep our counter in state as the variable count:

Now we pass the CounterState as the second parameter to the React.Component generic:

Looking at the above code, you might be wondering, how would I ever know that state was the second generic parameter?

It's in the documentation (for the types). Just like how you learn the order of function parameters in, say setTimeout, you look at the docs to learn the order of parameters for generics.

Functional Components#

Typing class components is interesting, but they're also falling out of fashion. So how would we type functional components?

Well, the first way is that you don't have to add any types at all.

Because we are returning JSX, TypeScript can automatically conclude that the return type of our function is JSX.Element, which is good enough for React.

Of course, if you want to be verbose, you can use React.FC:

But what about props?

Well, with a functional component, props are just arguments so we can just type the arguments:

Above we type the props as CounterProps. However, a more idomatic React style would be to destructure the props into variables like this:

What about functional state?

When we have a functional component we use hooks to manage state. Specifically we're going to use the useState hook.

Here's what our component looks like with keeping a basic counter with useState:

So in this case, if I was keeping simple values we don't need to add any extra types at all. If we keep one value within useState, it's pretty easy.

Buuuuut if you've worked with React long enough you're probably suspicious of my solution here. What about in the more complicated case where our state is an object, like in our class above?

Now, I'd like to point out, that in such a simple case I probably wouldn't store an object in useState as a matter of style. But I want to show you a more complicated case as it represents something that might come up in your work.

Say we want our state to be the same CounterState object:

To type this, we use the generics on useState:

Above, notice the line with useState<CounterState> - there, we're telling TypeScript that the object in useState is of type CounterState - and so it needs to enforce that "shape" on any value it controls.

So there you have it, that's the absolute basics of using React with TypeScript. You can get pretty far on just the above tips.

But there's still a lot more to learn like:

  • How do we type more complex hooks like useReducer?

  • How do we add types for a custom React library?

  • What if we're using GraphQL and we want typed queries?

We cover the answers to these questions and more in Fullstack React with TypeScript.