Skip to content

Contact sales

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

How to Use React Router in Typescript

Mar 6, 2019 • 13 Minute Read

How to Use React Router in Typescript

React Router is one of the most popular routing libraries in the React ecosystem. It is a well thought out library with an extensive test suite and support for browser, react-native, and server-side rendering. Before proceeding with this guide I would highly recommend going over the official React Router Guide to familiarize yourself with the terminology. The documentation provides exhaustive examples of all the tasks that React Router can help you with but doesn't include information about using it with Typescript, so that's what we'll discuss in this tutorial.

Introduction

In order to better understand the benefits that Typescript can bring when working with React Router, let's first review a simple example written in plain Javascript.

We'll start by creating an empty application using Create React App by running:

      npm install -g create-react-app
create-react-app demo-app
cd demo-app
    

As I've mentioned before, React Router supports several backends (dom, server, native) so you need to choose an appropriate NPM package to install. We'll be working on a Web application, so first we'll need to install react-router-dom:

      npm install -S react-router-dom
    

We'll add a simple two-page navigation system by modifying the src/App.js file and replacing its contents with:

      1 import React from "react";
2 import { BrowserRouter as Router, Route, Link } from "react-router-dom";
3 
4  function Index() {
5    return <h2>Home</h2>;
6  }
7 
8  function Product({ match }) {
9   return <h2>This is a page for product with ID: {match.params.id} </h2>;
10 }
11 
12 function AppRouter() {
13   return (
14     <Router>
15       <div>
16         <nav>
17           <ul>
18             <li>
19               <Link to="/">Home</Link>
20             </li>
21             <li>
22               <Link to="/products/1">First Product</Link>
23             </li>
24             <li>
25               <Link to="/products/2">Second Product</Link>
26             </li>
27           </ul>
28         </nav>
29 
30         <Route path="/" exact component={Index} />
31         <Route path="/products/:id" component={Product} />
32       </div>
33     </Router>
34   );
35 }
36 
37 export default AppRouter;
    

Let's start the application server:

      npm start
    

When you visit https://localhost:3000 you will see:

And if you visit https://localhost:3000/product/2 or click one of the product links you will get:

Review

Let's review what happens here:

  1. On line 30 we define a Home route that responds to the root URL "/" and is rendered using the Index component.

  2. On line 31 we define a Product route that responds to any URL that starts with "/product/" and has product's ID in its path. It is rendered using the Product component.

  3. The above Routes are wrapped by the Router component (line 14) which will decide what should be rendered based on the current location (URL path, parameters, and query arguments).

  4. The home route is a simple stateless component (line 4) which returns the page title.

  5. The product route receives an 'id' parameter, so in order to use it, we spread the component props and take the match prop which will give us access to all route parameters (line 8). We then render the page title by using the parameter.

So, what could cause an issue here? Let's add a small typo in our code and change match on lines 8 and 9 to matches.

The code still compiles fine, but when run it will generate the following error:

This rather confusing error message could've been avoided by introducing types to our code and performing static code analysis before it runs.This is where Typescript comes in.

Typescript

Introduction

To quote Wikipedia:

TypeScript is an open-source programming language developed and maintained by Microsoft. It is a strict syntactical superset of JavaScript, and adds optional static typing to the language.

It allows us to specify exactly what our object's shape is and what parameter type we expect to receive in a function, thus adding a layer of safety to the code we write.

To add TypeScript to a Create React App project, first install it:

      npm install --save-dev typescript @types/node @types/react @types/react-dom @types/jest @types/react-router-dom
    

Next, rename any file to be a TypeScript file (e.g. src/index.js to src/index.tsx) and restart your development server!

Immediately after the dev server starts you will get an error:

      Failed to compile.

C:/temp/demo-app/src/App.tsx
Type error: Binding element 'matches' implicitly has an 'any' type.  TS7031

     6 | }
     7 |
  >  8 | function Product({ matches }) {
       |                    ^
     9 |   return <h2>This is a page for product with ID: {matches.params.id} </h2>;
    10 | }
    11 |
    

Wonderful! Our typo has been caught. Now, let's see how to resolve the issue.

Adding Types

Before we begin, I want to bring your attention to the npm install command that we've run before. In addition to adding the Typescript compiler, we've also installed several @types packages. What are those? Each @types package contains inside it several .d.ts files which provide the actual type information for libraries installed from NPM.

For example, when you use a popular query-string library in your code:

      const queryString = require('query-string');

console.log(location.search);
//=> '?foo=bar'

const parsed = queryString.parse(location.search);
console.log(parsed);
//=> {foo: 'bar'}
    

The Typescript compiler has no way of knowing if the parameter you passed to queryString.parse method is the correct type. You can call queryString.parse(123) which will be a valid code, but will crash on run because the function expects to get a string for parsing.

To mitigate this issue, a separate package containing the type definitions exists and can be installed along-side the main package to augment it:

      npm install --save-dev @types/query-string
    

This package contains an index.d.ts file with the following code:

      export function parse(str: string, options?: ParseOptions): OutputParams;
    

It tells the compiler: I have a parse function that accepts up to two parameters. The first one is a required parameter of type string and the optional second parameter is called options. Now, calling queryString.parse with anything but a string will give you a syntax error!

Now that we've familiarized ourselves with the basics of using type packages, let's annotate our code in order to describe what parameters we are expecting and how we are handling them. We'll start by taking a closer look at the Product stateless component:

      function Product({ match }) {
   return <h2>This is a page for product with ID: {match.params.id} </h2>;
 }
    

Every stateless component in React is defined as a function that receives its props and returns a React element. In our case, we use JSX to generate that element and use the spread syntax to grab the match prop. How did we know that the props contain the match object? Well, we've read the documentation. In Typescript world, we have a better way.We can inspect the @types package and learn about the actual types of the parameters as React Router expects to see them.

Let's open the node_modules/@types/react-router/index.d.ts file and look for the Route definition:

      export interface RouteComponentProps<Params extends { [K in keyof Params]?: string } = {}, C extends StaticContext = StaticContext, S = H.LocationState> {
  history: H.History;
  location: H.Location<S>;
  match: match<Params>;
  staticContext?: C;
}
export interface RouteProps {
  location?: H.Location;
  component?: React.ComponentType<RouteComponentProps<any>> | React.ComponentType<any>;
  render?: ((props: RouteComponentProps<any>) => React.ReactNode);
  children?: ((props: RouteChildrenProps<any>) => React.ReactNode) | React.ReactNode;
  path?: string | string[];
  exact?: boolean;
  sensitive?: boolean;
  strict?: boolean;
}
export class Route<T extends RouteProps = RouteProps> extends React.Component<T, any> { }
    

We see that a Route is a React.Component that receives props in the shape of RouteProps. It then renders the component provided to it by component prop and defines what props it will pass to that component: RouteComponentProps.

When we inspect RouteComponentProps we see that it contains all the information we expect a Route to have: location, history, and the match prop.

Let's get back to our code and bring in the RouteComponentProps type and apply it to our Product function parameter.

We start by importing the type from the react-router-dom package:

      import {
  BrowserRouter as Router,
  Route,
  Link,
  RouteComponentProps
} from "react-router-dom";
    

and then annotating our function parameter:

      type TParams = { id: number };

function Product({ match }: RouteComponentProps<TParams>) {
  return <h2>This is a page for product with ID: {match.params.id} </h2>;
}
    

Basically we 'explained' to the compiler that:

  1. The function receives one parameter.
  2. The parameter is of type RouteComponentProps.
  3. The contents of the match field inside the props will be of type TParams.
  4. TParams is defined as an object that has an id field of type number.

All done, right? Not exactly. Our dev server is throwing an error:

      Type error: Type 'TParams' does not satisfy the constraint '{ id?: string | undefined; }'.                           
  Types of property 'id' are incompatible.                                                                           
    Type 'number' is not assignable to type 'string | undefined'.  TS2344                                            
                                                                                                                     
    13 | type TParams = { id: number };                                                                              
    14 |                                                                                                             
  > 15 | function Product({ match }: RouteComponentProps<TParams>) {                                                 
       |                                                 ^                                                           
    16 |   return <h2>This is a page for product with ID: {match.params.id} </h2>;                                   
    17 | }                                                                                                           
    18 |
    

Aha! We thought that since we expect id to be a number, we can declare it as such. But the compiler is warning us - Type 'number' is not assignable to type 'string | undefined'. Meaning: when React Router parses path parameters, it doesn't do any type conversion, nor does it have any way of knowing what is the actual type of id parameter, so it declares as string | undefined.

Let's fix our TParams definition to match:

      type TParams =  { id: string };
    

Voila, the code compiles and we have a peace of mind knowing that next time we'll try to do anything unexpected while passing routing parameters - the compiler will be on our side and will warn us right away!

Closing Words

Typescript can bring a new layer of safety into your code and I highly recommend giving it a try. Due to the dynamic nature of the Javascript language, it's sometimes rather hard to trace problems in your code due to simple typos or unexpected parameters being passed to an external library. Type definitions help to avoid that. You can learn a lot about the libraries you use by taking a peek inside their type definition .d.ts files and find out the proper way of using them.

Good luck!