Strong Typed React Router
I love to build apps using TypeScript and React. I’ve tried React Router on a few occasions, but I’ve usually had trouble figuring out how to tie my route matching paths to component props in a strong typed way that I felt good about. I think I finally found a configuration that I like which is the topic of this post.
The Setup
I am currently working on a scheduling app. At the moment it’s pretty simple and has only 2 routes.
- ’/’ - routes to a
ScheduleList
component - ‘/schedule:id’ - routes to a
Schedule
component
Each of my routes maps to a top level component. Their props look like:
interface ScheduleListProps {}
interface ScheduleProps {
id: string
}
I then have a TypeScript interface that defines the mapping of route matching paths to component props. Since the keys are treated as string literals, this mapping is strongly typed:
/** Map route to component props type */
interface RouteParams {
'/': {}
'/schedule/:id': { id: string }
}
The top level router of my app looks something like:
<Router>
<PrimaryNav />
<CustomRoute path="/" exact={true} component={ScheduleList} />
<CustomRoute path="/schedule/:id" component={Schedule} />
</Router>
Notice that I am using a CustomRoute
component. The Route
component that comes with react-router-dom
passes a nested object as props to the component designated by the component
prop, so I wrote a custom component more tailored to my use case.
Custom Route Component
My CustomRoute
component does 2 primary things
- Enforces the relationship of path matching patterns to component props
- Passes any parameters extracted from the route as props to the corresponding component
To pull this off I created a few helper types.
/** This is just a union type of my route matching strings */
type RoutePath = keyof RouteParams
/** Helper type to derive route props from path */
type Params<TPath extends RoutePath> = TPath extends RoutePath
? RouteParams[TPath]
: never
- RoutePath - union type of all of my route matching paths
- Params - helper type to infer prop types from given matching path
Now for the custom route component.
import React from 'react'
import * as ReactRouter from 'react-router-dom'
...
/** Override RouteProps with generics */
interface CustomRouteProps<TPath extends RoutePath>
extends Omit<ReactRouter.RouteProps, 'component' | 'path'> {
// tie our component type to our path type
component: React.ComponentType<Params<TPath>>
path: TPath
}
/**
* Route wrapper component that extracts route params
* and passes them to the given component prop.
*/
function CustomRoute<TPath extends RoutePath>({
component: Component,
...rest
}: CustomRouteProps<TPath>) {
return (
<ReactRouter.Route
{...rest}
render={({ match: { params } }) => <Component {...params} />}
/>
)
}
The code here is a little dense, so I’ll try to unpack it a bit.
CustomRouteProps extends the RouteProps that come with @types/react-router-dom
. It does so by omitting the component and path props and replacing them with ones tied to the generic TPath
arg. This is where the path types actually get tied to the component prop types.
The CustomRoute component is just a wrapper around the Route component provided by react router. It uses CustomRouteProps to map paths to prop types and also spreads the match params to the component so that it only gets the props I care about.
The Result
The result is that if I pass an untyped path to a Route component, the TypeScript compiler will complain.
<CustomRoute path="/invalid" component={Schedule} />
The compiler will also complain if I pass a component whose props don’t map to the given path. For example my Schedule
component takes a single id
prop.
export interface ScheduleProps {
id: string
}
const Schedule: React.FC<ScheduleProps> = ({ id }) => {
return <div>...</div>
}
If I pass it to my home route, the compiler will complain, since the path provides no args, and my component expects an id.
<CustomRoute path="/" component={Schedule} exact={true} />
Conclusion
I can now use the TypeScript
compiler to enforce my route mappings. This gives me extra protection as I add more routes, change route patterns or component props. Hope this is helpful to others as well. Peace.