Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

How to Statically Type React Components with TypeScript

Dec 21, 2018 • 11 Minute Read

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:

      npm install @types/react --dev
    

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:

      import * as React from "react";
    

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:

      declare class Component<P> {
    props: P
}
    

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:

      interface Props {
    products: string[];
}

interface State {
    quantities: { [key: string]: number };
}

class ShoppingBasket extends React.Component<Props, State> {

    static defaultProps: Props = {
        products: []
    }

    state: Readonly<State> = {
        quantities: this.props.products.reduce((acc, product) => {
            acc[product] = 1;
            return acc;
        }, {})
    }

    render() {

        const { products } = this.props;
        const { quantities } = this.state;

        return (
            <div>
                <ul>
                    {products.map(product =>
                        <li>
                            <h2>{product}</h2>
                            <p>
                                Quantity: 
                                <input 
                                    type="number" 
                                    value={quantities[product]}
                                />
                            </p>
                        </li>
                    )}
                </ul>
            </div>
        )
    }
}
    

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:

      state: Readonly<State> = {
    

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:

      static defaultProps: Props = {
    products: []
}
    

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:

      const ProductDisplay = (props: { title: string }) => (
    <h2>{props.title}</h2>
);

function ProductDisplay(props: { title: string }) {
    return (
        <h2>{props.title}</h2>
    )
}
    

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:

      const ProductDisplay: React.FunctionComponent<{ title: string}> = (props) => (
    <h2>{props.title} {props.children}</h2>
);

function ProductDisplay(props): React.FunctionComponent<{ title: string}> {
    return (
        <h2>{props.title} {props.children}</h2>
    )
}
    

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:

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

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:

      class ShoppingBasket extends React.Component<Props, State> {

    state: Readonly<State> = {
        quantities: this.props.products.reduce((acc, product) => {
            acc[product] = 1;
            return acc;
        }, {})
    }

    render() {

        const { products } = this.props;
        const { quantities } = this.state;

        return (
            <div>
                <ul>
                    {products.map(product =>
                        <li>
                            <ProductDisplay title={product} />
                            <p>
                                Quantity: 
                                <input 
                                    type="number" 
                                    value={quantities[product]}
+                                   onChange={this.onQuantityChanged(product)}
                                />
                            </p>
                        </li>
                    )}
                </ul>
            </div>
        )
    }
+
+   onQuantityChanged = (product: string) => 
+       (e: React.ChangeEvent<HTMLInputElement>) => {
+           const quantity = parseInt(e.target.value, 10);
+           this.setState({
+               quantities: {
+                   ...this.state.quantities,
+                   [product]: quantity
+               }
+           });
+       }
}
    

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:

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

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:

      class ShoppingBasket extends React.Component<Props, State> {

    static getDerivedStateFromProps: React.GetDerivedStateFromProps<Props, State> = (props, state) => {
        return { quantities: state.quantities }
    }

    componentDidUpdate(prevProps: Props, prevState: State, snapshot: any) {

    }

}
    

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:

      interface Snapshot {
    scrollHeight: number
}

class ShoppingBasket extends React.Component<Props, State, Snapshot> {

    getSnapshotBeforeUpdate(prevProps: Props, prevState: State) {
        return { scrollHeight: 10 }
    }

    componentDidUpdate(prevProps: Props, prevState: State, snapshot: Snapshot) {
        console.log(snapshot.scrollHeight); // strongly-typed access to snapshot data
    }

}
    

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