Type Safety

TanStack Router is built to be as type-safe as possible within the limits of the TypeScript compiler and runtime. This means that it's not only written in TypeScript, but that it also fully infers the types it's provided and tenaciously pipes them through the entire routing experience.

Ultimately, this means that you write less types as a developer and have more confidence in your code as it evolves.

Route Definitions

File-based Routing

Routes are hierarchical, and so are their definitions. If you're using file-based routing, much of the type-safety is already taken care of for you.

Code-based Routing

If you're using the Route class directly, you'll need to be aware of how to ensure your routes are typed properly using the Route's getParentRoute option. This is because child routes need to be aware of all of their parent routes types. Without this, those precious search params you parsed out of your layout route 3 levels up would be lost to the JS void.

So, don't forget to pass the parent route to your child routes!

tsx
const parentRoute = createRoute({
  getParentRoute: () => parentRoute,
})
const parentRoute = createRoute({
  getParentRoute: () => parentRoute,
})

Exported Hooks, Components, and Utilities

For the types of your router to work with top-level exports like Link, useNavigate, useParams, etc. they must permeate the type-script module boundary and be registered right into the library. To do this, we use declaration merging on the exported Register interface.

ts
const router = createRouter({
  // ...
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}
const router = createRouter({
  // ...
})

declare module '@tanstack/react-router' {
  interface Register {
    router: typeof router
  }
}

By registering your router with the module, you can now use the exported hooks, components, and utilities with your router's exact types.

Fixing the Component Context Problem

Component context is a wonderful tool in React and other frameworks for providing dependencies to components. However, if that context is changing types as it moves throughout your component hierarchy, it becomes impossible for TypeScript to know how to infer those changes. To get around this, context-based hooks and components require that you give them a hint on how and where they are being used.

tsx
export const Route = createFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  // Each route has type-safe versions of most of the built-in hooks from TanStack Router
  const params = Route.useParams()
  const search = Route.useSearch()

  // Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
  // we must pass the `from` param to tell the hook our relative position in the route hierarchy.
  const navigate = useNavigate({ from: Route.fullPath })
  // ... etc
}
export const Route = createFileRoute('/posts')({
  component: PostsComponent,
})

function PostsComponent() {
  // Each route has type-safe versions of most of the built-in hooks from TanStack Router
  const params = Route.useParams()
  const search = Route.useSearch()

  // Some hooks require context from the *entire* router, not just the current route. To achieve type-safety here,
  // we must pass the `from` param to tell the hook our relative position in the route hierarchy.
  const navigate = useNavigate({ from: Route.fullPath })
  // ... etc
}

Every hook and component that requires a context hint will have a from param where you can pass the ID or path of the route you are rendering within.

🧠 Quick tip: If your component is code-split, you can use the getRouteApi function to avoid having to pass in the Route.fullPath to get access to the typed useParams() and useSearch() hooks.

What if I don't know the route? What if it's a shared component?

The from property is optional, which means if you don't pass it, you'll get the router's best guess on what types will be available. Usually, that means you'll get a union of all of the types of all of the routes in the router.

What if I pass the wrong from path?

It's technically possible to pass a from that satisfies TypeScript, but may not match the actual route you are rendering within at runtime. In this case, each hook and component that supports from will detect if your expectations don't match the actual route you are rendering within, and will throw a runtime error.

What if I don't know the route, or it's a shared component, and I can't pass from?

If you are rendering a component that is shared across multiple routes, or you are rendering a component that is not within a route, you can pass strict: false instead of a from option. This will not only silence the runtime error, but will also give you relaxed, but accurate types for the potential hook you are calling. A good example of this is calling useSearch from a shared component:

tsx
function MyComponent() {
  const search = useSearch({ strict: false })
}
function MyComponent() {
  const search = useSearch({ strict: false })
}

In this case, the search variable will be typed as a union of all possible search params from all routes in the router.

Router Context

Router context is so extremely useful as it's the ultimate hierarchical dependency injection. You can supply context to the router and to each and every route it renders. As you build up this context, TanStack Router will merge it down with the hierarchy of routes, so that each route has access to the context of all of its parents.

The createRootRouteWithContext factory creates a new router with the instantiated type, which then creates a requirement for you to fulfill the same type contract to your router, and will also ensure that your context is properly typed throughout the entire route tree.

tsx
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // ... all child routes will have access to `whateverYouWant` in their context
])

const router = createRouter({
  routeTree,
  context: {
    // This will be required to be passed now
    whateverYouWant: true,
  },
})
const rootRoute = createRootRouteWithContext<{ whateverYouWant: true }>()({
  component: App,
})

const routeTree = rootRoute.addChildren([
  // ... all child routes will have access to `whateverYouWant` in their context
])

const router = createRouter({
  routeTree,
  context: {
    // This will be required to be passed now
    whateverYouWant: true,
  },
})
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.