Type-Safe Usage of React Router

Type-Safe Usage of React Router:

 

This is my approach to implement strongly typed routing using React Router and TypeScript. So that if I try to create a  to an unknown path, tsc can warn me appropriately. Of course there are other benefits of typed routes, but let’s go over what’s wrong with the current implementation first.

Problem

  1. react-router takes any plain string as a path. This makes it difficult to refactor routes when it is required to rename/delete/add routes. Also typos are hard to detect.
  2. Developers need to provide types for useParams hook (i.e. useParams). It has the same issue with refactoring. Developers need to update useParams hooks whenever there’s a change in URL parameter names.

Solution (Walkthrough)

I ended up implementing something I am happy with. Example source code is available on a GitHub repo. I hope this can help others who desire typed routes. This post is mostly annotation of my implementation, so if you prefer reading source code directly, check out the GitHub repo.

src/hooks/paths.tsx

The single source of truth for available paths is defined in this module. If a route needs to be modified, this PATH_SPECS can be fixed, then TypeScript compiler will raise errors where type incompatibilities are found.

const PATHS = [
  '/',
  '/signup',
  '/login',
  '/post/:id',
  '/calendar/:year/:month',
] as const;

Utility types can be derived from this readonly array of paths.

type ExtractRouteParams = string extends T
    ? Record
    : T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? { [k in Param | keyof ExtractRouteParams]: string }
    : T extends `${infer _Start}:${infer Param}`
    ? { [k in Param]: string }
    : {};

export type Path = (typeof PATHS)[number];

// Object which has matching parameter keys for a path.
export type PathParams

= ExtractRouteParams

;

Small amount of TypeScript magic is applied here, but the end result is quite simple. Note how PathParams type behaves.

  • PathParams is { id: string }
  • PathParams is { year: string, month: string }
  • PathParams is {}

From here, a type-safe utility function is written for building URL strings.

/**
* Build an url with a path and its parameters.
* @example
* buildUrl(
* '/a/:first/:last',
* { first: 'p', last: 'q' },
* ) // returns '/a/p/q'
* @param path target path.
* @param params parameters.
*/
export const buildUrl = 

( path: P, params: PathParams

, ): string => { let ret: string = path; // Upcast `params` to be used in string replacement. const paramObj: { [i: string]: string } = params; for (const key of Object.keys(paramObj)) { ret = ret.replace(`:${key}`, paramObj[key]); } return ret; };

buildUrl function can be used like this:

buildUrl(
  '/post/:id',
  { id: 'abcd123' },
); // returns '/post/abcd123'

buildUrl only takes a known path (from PATHS) as the first argument, therefore typo-proof. Sweet!

src/components/TypedLink

Now, let’s look at TypedLink a type-safe alternative to Link.

import { Path, PathParams, buildUrl } from '../hooks/paths';
import React, { ComponentType, ReactNode } from 'react';

import { Link } from 'react-router-dom';

type TypedLinkProps

= { to: P, params: PathParams

, replace?: boolean, component?: ComponentType, children?: ReactNode, }; /** * Type-safe version of `react-router-dom/Link`. */ export const TypedLink =

({ to, params, replace, component, children, }: TypedLinkProps

) => { return ( {children} ); }

TypedLink can be used like this:


Enter fullscreen mode Exit fullscreen mode

The to props of TypedLink only takes a known path, just like buildUrl.

src/components/TypedRedirect.tsx

TypedRedirect is implemented in same fashion as TypedLink.

import { Path, PathParams, buildUrl } from '../hooks/paths';

import React from 'react';
import { Redirect } from 'react-router-dom';

type TypedRedirectProps

= { to: P, params: PathParams

, push?: boolean, from?: Q, }; /** * Type-safe version of `react-router-dom/Redirect`. */ export const TypedRedirect =

({ to, params, push, from, }: TypedRedirectProps

) => { return ( ); };

src/hooks/index.tsx

Instead of useParams which cannot infer the shape of params object, useTypedParams hook can be used. It can infer the type of params from pathparameter.

/**
* Type-safe version of `react-router-dom/useParams`.
* @param path Path to match route.
* @returns parameter object if route matches. `null` otherwise.
*/
export const useTypedParams = 

( path: P ): PathParams

| null => { // `exact`, `sensitive` and `strict` options are set to true // to ensure type safety. const match = useRouteMatch({ path, exact: true, sensitive: true, strict: true, }); if (!match || !isParams(path, match.params)) { return null; } return match.params; }

Finally, useTypedSwitch allows type-safe  tree.

/**
* A hook for defining route switch.
* @param routes * @param fallbackComponent */
export const useTypedSwitch = (
  routes: ReadonlyArray,
  fallbackComponent?: ComponentType,
): ComponentType => {
  const Fallback = fallbackComponent;
  return () => (
    
      {routes.map(({ path, component: RouteComponent }, i) => (
        
          
        
      ))}
      {Fallback && }
    
  );
}

Here’s how  is usually used:

// Traditional approach.
const App = () => (
  
    
      
      
    
  
);
following code.
const App = () => {
  const TypedSwitch = useTypedSwitch([
    { path: '/', component: Home },
    { path: '/user/:id', component: User },
  ]);

  return (
    
      
    
  );
}

Conclusion

Original Replaced
useParams() useTypedParams('/user/:id')
useTypedSwitch

Type-safe alternatives are slightly more verbose than the original syntax, but I believe this is better for overall integrity of a project.

  • Developers can make changes in routes without worrying about broken links (at least they don’t break silently).
  • Nice autocompletion while editing code.

from Tumblr https://generouspiratequeen.tumblr.com/post/636932910593802240

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s