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.
To access React TypeScript typings, ensure your TypeScript-based React project has the @types/react package installed:
1npm 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:
1import * as React from "react";
The official Facebook create-react-app package supports TypeScript out-of-the-box.
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}
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}
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> = {
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}
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}
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.
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}
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} />
TypeScript will now throw a compiler error if the wrong type of value is used for the title
prop.
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}
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}
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.
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}
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 likeReact.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}
You can read more about component snapshots in the official docs.
The full working sample which you can modify or view on your own can be found here on CodeSandbox: https://codesandbox.io/s/m5prrr48zy