Go to main content
January 20, 2024
Cover image

In my previous article Better developper experience with full typesafe helper functions we saw that to creating helper functions help us to have full typesafety.

And that’s a principle that is used in most of the Tanstack libraries: @tanstack/table, @tanstack/form thanks to hook helper.

But is it the same for @tanstack/router?

When I looked at the documentation on November 2022, it used an helper function!

A createReactRouter function was exported that exposes all usefull component/hook that are full typesafe based on the routes project configuration:

  • Link
  • useRoute

Example of code:

import {
  Outlet,
  RouterProvider,
  createReactRouter,
  createRouteConfig,
} from '@tanstack/react-router'

const routeConfig = createRouteConfig().createChildren((createRoute) => [
  createRoute({
    path: '/',
    component: Index,
  }),
  createRoute({
    path: 'about',
    component: About,
  }),
])

const router = createReactRouter({ routeConfig })

function App() {
  return (
    <>
      <RouterProvider router={router}>
        <div>
          <router.Link to="/">Home</router.Link>{' '}
          <router.Link to="/about">About</router.Link>
        </div>
        <hr />
        <Outlet />
      </RouterProvider>
    </>
  )
}

But if you look at the doc right now, this is not the same API. Now you can directly import Link from @tanstack/router and it’s typesafe!

How does it work?


I won’t make a debate on which one you have to use but will concentrate on a major difference between them.

What happens if you declare two time a same type/interface?

When dealing with type, you will have an error:

// Error: Duplicate identifier 'Person'
type Person = {
  firstname: string;
};

// Error: Duplicate identifier 'Person'
type Person = {
  lastname: string;
};

In other way if you do the same with interface, it will work. This is called Declaration merging , Typescript compiler merge all definitions in one interface:

interface Person {
  firstname: string;
}

interface Person {
  lastname: string;
}

// Person is { firstname: string; lastname: string}
const person: Person = {
  firstname: 'Bob',
  lastname: 'TheSponge',
};

Do you see it coming?


The @tanstack/router API is based on Declaration mergin.

You have to override an interface Register exposed by the library:

import {
  Outlet,
  RouterProvider,
  Link,
  Router,
  Route,
  RootRoute,
} from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/router-devtools'

const rootRoute = new RootRoute()

const indexRoute = new Route({
  getParentRoute: () => rootRoute,
  path: '/',
  component: function Index() {
    return (
      <div>
        Home
      </div>
    )
  },
})

const aboutRoute = new Route({
  getParentRoute: () => rootRoute,
  path: '/about',
  component: function About() {
    return <div>About</div>
  },
})

const routeTree = rootRoute.addChildren([indexRoute, aboutRoute])

const router = new Router({ routeTree })

// Here is one of the smartest thing I saw <3
declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

And the interface in the library is:

// Make sure to export it
export interface Register {}

Yep no implementation because it’s to the project to define it.

There is a nice trick to get the user routes defintion from Register thanks to infer and extends:

type RegisteredRouter = Register extends {
  router: infer TRouter extends AnyRouter;
}
  ? TRouter
  : AnyRouter;

It’s time to get our hands dirty. Here I am going to do a simple implementation to show you what was describes in the previous parts.

The type of Route is pretty simple:

type Route<TPath extends string> = {
  path: TPath;
  component: FunctionComponent;
};

type Routes<TPath extends string> = Route<TPath>[];

We make a utility function to create routes:

function createRoutes<TPath extends string>(
  routes: Routes<TPath>,
) {
  // Here we could override routes with lib stuff
  // But here for simplicity, do nothing
  return routes;
}

And it’s now time to do the magic trick:

// Make sure to export the interface!
export interface Register {}

type AnyRoutes = Routes<string>;

type RegisteredRoutes = Register extends {
  routes: infer Routes extends AnyRoutes;
}
  ? Routes
  : AnyRoutes;

And the fully type safe Link component becomes:

type LinkProps = {
  to: RegisteredRoutes[number]["path"];
  children: ReactNode;
};

function Link(props: LinkProps) {
  const { to, children } = props;

  return (
    <a
      href={to}
      onClick={(e) => {
        e.preventDefault();

        history.pushState(undefined, "", to);
      }}
    >
      {children}
    </a>
  );
}

Let’s create some routes to see that the Link component is completely type safe:

import { createRoutes, Link } from "@rainbow/router";

const routes = createRoutes([
  {
    path: "/",
    component: () => <h1>Home page</h1>,
  },
  {
    path: "/about",
    component: () => <h1>About page</h1>,
  },
]);

// Let's type `@rainbow/router` with the implementation of our routes
declare module "@rainbow/router" {
  interface Register {
    routes: typeof routes;
  }
}

function App() {
  return (
    <Link to="/about">Go to about page</Link>
    //     ^?    "/" | "/about"
  );
}

@tanstack/router made a revolution in the router world, I am looking forward to see the response from the Remix team to see if they are going to make react-router full typesafe.

Of course this trick works for library that are project wide, I mean for a form library it wouldn’t be the best solution because you would probably have more than 1 form.

If you are working in a monorepo with multiple applications , don’t be afraid the configuration of routes won’t be shared. As Tanner Linslay said:

Declaration merging works on a TS config level.

So you could even have different typed router in a same application if you put multiple tsconfig.json, for example I made this application code .


🔥 Tanstack Router .

💻 Simple Typesafe Router repository .

🎥 The Future of Routing w/ Tanner Linsley by Ryan Carniato.

📝 Type vs Interface: Which Should You Use? by Matt Pocock.

📚 Module Augmentation .


You can find me on Twitter if you want to comment this post or just contact me. Feel free to buy me a coffee if you like the content and encourage me.