Type-safe Router

Quick introduction

Hi! I’m getting more active in the Redwood community because I feel this project has a lot of potential. My focus would be type-safety as you probably have seen my work on TypeScript Project References PR.

Typed Routers

Type safety in routing can be massively beneficial. I’ve encountered so many bugs in production because routes where not on the same page as other parts of the codebase.

Routers such as React Router provide typing systems but they are not fully integrated. Redwood is in a unique position to offer a better more integrated and statically analyzed solution.

State of the art

I’ve used React Router and it’s types from Defiantly Typed. They usually work by handwriting your route type interface:

<Switch>
  <Route path="/rooms/{id}" component={Room} />
</Switch>

And then in Room component you must define the id manually:

import {RouteComponentProps} from 'react-router';

const Room: React.FC<RouteComponentProps<{ id: string; }>> = () => null;

There is no connection between where the route is defined and where the TypeScript interface for the route is defined. if I decide to rename the parameter in the route the TypeScript definition will be out of date:

  <Route path="/rooms/{roomId}" component={Room} />

There is no static analysis system to catch this issue.

Redwood Router

Redwood Routes has the opportunity of statically analyzing the route definitions and generate type definitions based on the routes. A great design decision that has been made is that routes are imported from @redwood/router. This enables Redwood to emit type definitions for the routes export every time the route definitions change. Thankfully route parameters have a type interface (Int for example). This makes the route type definition even more strongly typed as oppose to React Router in which route params are always string.

Generating route type definitions also benefits JavaScript users that use editors like VSCode. The routes will show up in editor suggestion as user types in.

Nice thing about generating types into node_modules is that nothing from the user perspective changes!

The same example above will be:

import { Router, Route } from '@redwoodjs/router'

const Routes = () => (
  <Router>
    <Route path="/rooms/{roomId: Int} page={RoomsPage} name="room" />
  </Router>
)

export default Routes
import { Link, routes } from '@redwoodjs/router'

const SomePage = () => <Link to={routes.room({ roomId: 123 })} />

And if I change the parameter name to id, I will get a type error in SomePage.

This is similar to my RFC for generating GraphQL operation types. I love how Prisma hides ugly generated types from users.

This is not an actual RFC because I have not looked into how we can generate the types. A few things I’m not sure about:

Main question is: Can we run a code gen step every time any route definition changes?

  • Will all routes be in a single file?
  • How dynamic path parameter can be? if it is too dynamic we can’t do static analysis
  • Maybe we can generate types when Route component is invoked? This is problematic because in dev time Route might never get invoked.
2 Likes

I looked into this a little bit. mapNamedRoutes in router is a good place to generate the types.
I was under the impression that Redwood runs the router in the backend too. Since Redwood doesn’t do SSR at least for now. We can’t use this function to generate types. We can however watch the routes.ts file and invoke the React component in it to get the list of routes and their parameters.

@mohsen definitely a :+1: to this. I’m not going to be much help regarding implementation suggestions, however.

@peterp do you agree this is worth stepping forward? If so, it makes sense to me for @mohsen to open a WIP/Draft PR that references this Forum topic. And instead of opening a new Issue, we could add to the TS Tracking issue here.