Author avatar

Kamran Ayub

How to Statically Type React Components with TypeScript

Kamran Ayub

  • Dec 21, 2018
  • 11 Min read
  • 217,261 Views
  • Dec 21, 2018
  • 11 Min read
  • 217,261 Views
React
TypeScript

Summary

In this guide, you will learn how to statically type React.js components using the compile-to-JavaScript language, TypeScript. Using TypeScript with React allows you to develop strongly-typed components that have identifiable props and state objects, which will ensure any usages of your components are type checked by the compiler.

The guide assumes you are already familiar with React and TypeScript as separate technologies but are now interested in using them together.

Referencing React Typings

To access React TypeScript typings, ensure your TypeScript-based React project has the @types/react package installed:

1npm install @types/react --dev
bash

The React typings package will allow you to import types from the react module that TypeScript will understand.

Start by importing React in your TypeScript file:

1import * as React from "react";
typescript

The official Facebook create-react-app package supports TypeScript out-of-the-box.

Typing Class Component Props and State

To implement a React class component, the classes to extend are React.Component<P, S> or React.PureComponent<P, S>.

These are generic types in TypeScript which provide an easy way to substitute types. For example, a simplistic type definition for React.Component<P> would look like:

1declare class Component<P> {
2    props: P
3}
typescript

You can read this syntax out loud as "Component of P" where P will be the props type substituted where referenced within the class definition. P can be any name, there is no rule, but a common naming convention is to use T as the prefix, like TProps.

For React.Component<P, S>, each generic argument corresponds to the props and state types respectively. There are no differences in TypeScript usage between a normal or pure component.

The following is a ShoppingBasket component that renders a list of products with their desired quantities. The props and state are described as TypeScript interfaces:

1interface Props {
2    products: string[];
3}
4
5interface State {
6    quantities: { [key: string]: number };
7}
8
9class ShoppingBasket extends React.Component<Props, State> {
10
11    static defaultProps: Props = {
12        products: []
13    }
14
15    state: Readonly<State> = {
16        quantities: this.props.products.reduce((acc, product) => {
17            acc[product] = 1;
18            return acc;
19        }, {})
20    }
21
22    render() {
23
24        const { products } = this.props;
25        const { quantities } = this.state;
26
27        return (
28            <div>
29                <ul>
30                    {products.map(product =>
31                        <li>
32                            <h2>{product}</h2>
33                            <p>
34                                Quantity: 
35                                <input 
36                                    type="number" 
37                                    value={quantities[product]}
38                                />
39                            </p>
40                        </li>
41                    )}
42                </ul>
43            </div>
44        )
45    }
46}
typescript

Notice how both the Props and State interfaces are specified as the generic parameters to the class type. Both are optional and will be an empty object ({}) by default. By specifying a type, TypeScript is able to strongly type this.props and this.state.

The following line explicitly annotates the component's state class property type:

1state: Readonly<State> = {
typescript

By explicitly typing the state property, we ensure the compiler throws an error if the initialized state object does not match the State interface. Using the Readonly<T> built-in type helper ensures TypeScript will throw an error if you attempt to modify this.state directly.

The same explicitness applies to the ShoppingBasket.defaultProps static class property:

1static defaultProps: Props = {
2    products: []
3}
typescript

Typing Functional Component Props

Rather than rendering the product title inline within the ShoppingBasket component, let's demonstrate how to statically type a React functional component by extracting the product display to its own separate component.

Functional components can be typed using both the const/let keyword or function styles:

1const ProductDisplay = (props: { title: string }) => (
2    <h2>{props.title}</h2>
3);
4
5function ProductDisplay(props: { title: string }) {
6    return (
7        <h2>{props.title}</h2>
8    )
9}
typescript

In this case, we don't need to define a new interface to describe ProductDisplay's props because we only pass the product title. Instead, we use an object type annotation with a title string property. You annotate a React functional component's props the same way as any other function in TypeScript.

Accessing Props Children in Functional Components

To access the React-provided prop children in a functional component, you can opt to use the React.FunctionComponent<TProps> type annotation:

1const ProductDisplay: React.FunctionComponent<{ title: string}> = (props) => (
2    <h2>{props.title} {props.children}</h2>
3);
4
5function ProductDisplay(props): React.FunctionComponent<{ title: string}> {
6    return (
7        <h2>{props.title} {props.children}</h2>
8    )
9}
typescript

Using this type annotation allows TypeScript to understand the context of the React component and augments the custom props with the default React-provided props like children.

Annotating the props function argument explicitly is redundant now as we provide it as the TProps generic parameter in the function return type.

Replace the inline product title now with the new ProductDisplay component:

1- <h2>{product}</h2>
2+ <ProductDisplay title={product} />
diff

TypeScript will now throw a compiler error if the wrong type of value is used for the title prop.

Typing React Event Handlers

To handle the quantity input's value change and update the ShoppingBasket state, add a new arrow function class property onQuantityChanged and hook into it via the input's onChange prop:

1class ShoppingBasket extends React.Component<Props, State> {
2
3    state: Readonly<State> = {
4        quantities: this.props.products.reduce((acc, product) => {
5            acc[product] = 1;
6            return acc;
7        }, {})
8    }
9
10    render() {
11
12        const { products } = this.props;
13        const { quantities } = this.state;
14
15        return (
16            <div>
17                <ul>
18                    {products.map(product =>
19                        <li>
20                            <ProductDisplay title={product} />
21                            <p>
22                                Quantity: 
23                                <input 
24                                    type="number" 
25                                    value={quantities[product]}
26+                                   onChange={this.onQuantityChanged(product)}
27                                />
28                            </p>
29                        </li>
30                    )}
31                </ul>
32            </div>
33        )
34    }
35+
36+   onQuantityChanged = (product: string) => 
37+       (e: React.ChangeEvent<HTMLInputElement>) => {
38+           const quantity = parseInt(e.target.value, 10);
39+           this.setState({
40+               quantities: {
41+                   ...this.state.quantities,
42+                   [product]: quantity
43+               }
44+           });
45+       }
46}
diff

By using an arrow function class property, the this keyword within the handler will be bound to the class instance. This means you do not need to add a bind call in the class constructor.

The onQuantityChanged function is a factory function that will return a React event handler for each given product. React event arguments are typically typed using the *Event suffix and take a generic argument representing the element being targeted.

In the case above, the onChange event is being added to a HTMLInputElement which is not React-specific and available through TypeScript's built-in typings. This enables us to access e.target.value which would otherwise throw a TypeScript compiler error.

Another common React event handler would be mouse events for onClick:

1function handleOnClick(e: React.MouseEvent) {
2    e.pageX; // access mouse event properties
3}
typescript

These events can be found in the @types/react package and all of them inherit from React.SyntheticEvent<T> base interface. See the React typings for all possible event types.

Lifecycle Methods and Other React APIs

React class components have lifecycle methods like componentDidUpdate and other APIs like getDerivedStateFromProps that can be strongly-typed with TypeScript:

1class ShoppingBasket extends React.Component<Props, State> {
2
3    static getDerivedStateFromProps: React.GetDerivedStateFromProps<Props, State> = (props, state) => {
4        return { quantities: state.quantities }
5    }
6
7    componentDidUpdate(prevProps: Props, prevState: State, snapshot: any) {
8
9    }
10
11}
typescript

To learn what TypeScript annotations should be used for each API, study the lifecycle API in the React docs and the corresponding typings.

Note: In many cases, the @types/react package provides type helpers like React.GetDerivedStateFromProps<P, S> shown above. Where possible use the provided type helpers instead of manually adding your own type annotations that may not stay in sync with official React releases.

For component snapshots, a third generic type argument can be passed to React.Component that represents the type used for the update snapshot as shown here:

1interface Snapshot {
2    scrollHeight: number
3}
4
5class ShoppingBasket extends React.Component<Props, State, Snapshot> {
6
7    getSnapshotBeforeUpdate(prevProps: Props, prevState: State) {
8        return { scrollHeight: 10 }
9    }
10
11    componentDidUpdate(prevProps: Props, prevState: State, snapshot: Snapshot) {
12        console.log(snapshot.scrollHeight); // strongly-typed access to snapshot data
13    }
14
15}
typescript

You can read more about component snapshots in the official docs.

Reference Code Sample

The full working sample which you can modify or view on your own can be found here on CodeSandbox: https://codesandbox.io/s/m5prrr48zy