| name | router-core/type-safety |
| description | Full type inference philosophy (never cast, never annotate inferred values), Register module declaration, from narrowing on hooks and Link, strict:false for shared components, getRouteApi for code-split typed access, addChildren with object syntax for TS perf, LinkProps and ValidateLinkOptions type utilities, as const satisfies pattern. |
| type | sub-skill |
| library | tanstack-router |
| library_version | 1.166.2 |
| requires | ["router-core"] |
| sources | ["TanStack/router:docs/router/guide/type-safety.md","TanStack/router:docs/router/guide/type-utilities.md","TanStack/router:docs/router/guide/render-optimizations.md"] |
Type Safety
TanStack Router is FULLY type-inferred. Params, search params, context, and loader data all flow through the route tree automatically. The #1 AI agent mistake is adding type annotations, casts, or generic parameters to values that are already inferred.
CRITICAL: NEVER use as Type, explicit generic params, satisfies on hook returns, or type annotations on inferred values. Every cast masks real type errors and breaks the inference chain.
CRITICAL: Do NOT confuse TanStack Router with Next.js or React Router. There is no getServerSideProps, no useSearchParams(), no useLoaderData() from react-router-dom.
The ONE Required Type Annotation: Register
Without this, top-level exports like Link, useNavigate, useSearch have no type safety.
import { createRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'
const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
export default router
After registration, every Link, useNavigate, useSearch, useParams across the app is fully typed.
Types Flow Automatically
Route Hooks — No Annotation Needed
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/posts/$postId')({
validateSearch: (search: Record<string, unknown>) => ({
page: Number(search.page ?? 1),
}),
loader: async ({ params }) => {
const post = await fetchPost(params.postId)
return { post }
},
component: PostComponent,
})
function PostComponent() {
const { postId } = Route.useParams()
const { page } = Route.useSearch()
const { post } = Route.useLoaderData()
return (
<div>
<h1>{post.title}</h1>
<p>Page {page}</p>
</div>
)
}
Context Flows Through the Tree
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
interface RouterContext {
auth: { userId: string; role: 'admin' | 'user' } | null
}
export const Route = createRootRouteWithContext<RouterContext>()({
component: () => <Outlet />,
})
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
beforeLoad: ({ context }) => {
if (!context.auth) throw redirect({ to: '/login' })
return { user: context.auth }
},
loader: ({ context }) => {
return fetchDashboard(context.user.userId)
},
component: DashboardComponent,
})
function DashboardComponent() {
const data = Route.useLoaderData()
const { user } = Route.useRouteContext()
return <h1>Welcome {user.userId}</h1>
}
Narrowing with from
Without from, hooks return a union of ALL routes' types — slow for TypeScript and imprecise.
On Hooks
import { useSearch, useParams, useNavigate } from '@tanstack/react-router'
function PostSidebar() {
const search = useSearch()
const search = useSearch({ from: '/posts/$postId' })
const { postId } = useParams({ from: '/posts/$postId' })
const navigate = useNavigate({ from: '/posts/$postId' })
}
On Link
import { Link } from '@tanstack/react-router'
<Link to=".." search={{ page: 0 }} />
<Link from="/posts/$postId" to=".." search={{ page: 0 }} />
<Link from={Route.fullPath} to=".." search={{ page: 0 }} />
Shared Components: strict: false
When a component is used across multiple routes, use strict: false instead of from:
import { useSearch } from '@tanstack/react-router'
function GlobalSearch() {
const search = useSearch({ strict: false })
return <span>Query: {search.q ?? ''}</span>
}
Code-Split Files: getRouteApi
Use getRouteApi instead of importing Route to avoid pulling route config into the lazy chunk:
import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'
const routeApi = getRouteApi('/posts')
export const Route = createLazyFileRoute('/posts')({
component: PostsComponent,
})
function PostsComponent() {
const data = routeApi.useLoaderData()
const { page } = routeApi.useSearch()
return <div>Page {page}</div>
}
TypeScript Performance
Use Object Syntax for addChildren in Large Route Trees
const routeTree = rootRoute.addChildren([
postsRoute.addChildren([postRoute, postsIndexRoute]),
indexRoute,
])
const routeTree = rootRoute.addChildren({
postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
indexRoute,
})
With file-based routing the route tree is generated, so this is handled for you.
Avoid Returning Unused Inferred Types from Loaders
When using external caches like TanStack Query, don't let the router infer complex return types you never consume:
export const Route = createFileRoute('/posts/$postId')({
loader: ({ context: { queryClient }, params: { postId } }) =>
queryClient.ensureQueryData(postQueryOptions(postId)),
component: PostComponent,
})
export const Route = createFileRoute('/posts/$postId')({
loader: async ({ context: { queryClient }, params: { postId } }) => {
await queryClient.ensureQueryData(postQueryOptions(postId))
},
component: PostComponent,
})
as const satisfies for Link Option Objects
Never use LinkProps as a variable type — it's an enormous union:
import type { LinkProps, RegisteredRouter } from '@tanstack/react-router'
const wrongProps: LinkProps = { to: '/posts' }
const goodProps = { to: '/posts' } as const satisfies LinkProps
const narrowedProps = {
to: '/posts',
} as const satisfies LinkProps<RegisteredRouter, string, '/posts'>
Type-Safe Link Option Arrays
import type { LinkProps } from '@tanstack/react-router'
export const navLinks = [
{ to: '/posts' },
{ to: '/posts/$postId', params: { postId: '1' } },
] as const satisfies ReadonlyArray<LinkProps>
export type NavLink = (typeof navLinks)[number]
Type Utilities for Generic Components
ValidateLinkOptions — Type-Safe Link Props in Custom Components
import {
Link,
type RegisteredRouter,
type ValidateLinkOptions,
} from '@tanstack/react-router'
interface NavItemProps<
TRouter extends RegisteredRouter = RegisteredRouter,
TOptions = unknown,
> {
label: string
linkOptions: ValidateLinkOptions<TRouter, TOptions>
}
export function NavItem<TRouter extends RegisteredRouter, TOptions>(
props: NavItemProps<TRouter, TOptions>,
): React.ReactNode
export function NavItem(props: NavItemProps): React.ReactNode {
return (
<li>
<Link {...props.linkOptions}>{props.label}</Link>
</li>
)
}
<NavItem label="Posts" linkOptions={{ to: '/posts' }} />
<NavItem label="Post" linkOptions={{ to: '/posts/$postId', params: { postId: '1' } }} />
ValidateNavigateOptions and ValidateRedirectOptions
Same pattern as ValidateLinkOptions above, for useNavigate and redirect. Declare a generic public overload plus a non-generic implementation signature so the call site stays narrowed and the body works without casts:
import {
useNavigate,
type RegisteredRouter,
type ValidateNavigateOptions,
} from '@tanstack/react-router'
export function useDelayedNavigate<
TRouter extends RegisteredRouter = RegisteredRouter,
TOptions = unknown,
>(
options: ValidateNavigateOptions<TRouter, TOptions>,
delayMs: number,
): () => void
export function useDelayedNavigate(
options: ValidateNavigateOptions,
delayMs: number,
) {
const navigate = useNavigate()
return () => {
setTimeout(() => navigate(options), delayMs)
}
}
ValidateRedirectOptions works identically — declare a generic overload accepting ValidateRedirectOptions<TRouter, TOptions> and an impl signature accepting ValidateRedirectOptions, then call redirect(options) in the body.
Render Props for Maximum Performance
Instead of accepting LinkProps, invert control so Link is narrowed at the call site:
function Card(props: { title: string; renderLink: () => React.ReactNode }) {
return (
<div>
<h2>{props.title}</h2>
{props.renderLink()}
</div>
)
}
;<Card title="All Posts" renderLink={() => <Link to="/posts">View</Link>} />
Render Optimizations
Fine-Grained Selectors with select
function PostTitle() {
const page = Route.useSearch({ select: ({ page }) => page })
return <span>Page {page}</span>
}
Structural Sharing
Preserve referential identity across re-renders for search params:
const router = createRouter({
routeTree,
defaultStructuralSharing: true,
})
const result = Route.useSearch({
select: (search) => ({ foo: search.foo, label: `Page ${search.foo}` }),
structuralSharing: true,
})
Structural sharing only works with JSON-compatible data. TypeScript will error if you return class instances with structuralSharing: true.
Common Mistakes
1. CRITICAL: Adding type annotations or casts to inferred values
const search = useSearch({ from: '/posts' }) as { page: number }
const params: { postId: string } = useParams({ from: '/posts/$postId' })
const data = useLoaderData<{ posts: Post[] }>({ from: '/posts' })
const search = useSearch({ from: '/posts' })
const params = useParams({ from: '/posts/$postId' })
const data = useLoaderData({ from: '/posts' })
2. HIGH: Using un-narrowed LinkProps type
const myProps: LinkProps = { to: '/posts' }
const myProps = { to: '/posts' } as const satisfies LinkProps
3. HIGH: Not narrowing Link/useNavigate with from
<Link to=".." search={{ page: 0 }} />
<Link from={Route.fullPath} to=".." search={{ page: 0 }} />
4. CRITICAL (cross-skill): Missing router type registration
const router = createRouter({ routeTree })
const router = createRouter({ routeTree })
declare module '@tanstack/react-router' {
interface Register {
router: typeof router
}
}
5. CRITICAL (cross-skill): Wrong-framework imports and file structure
Wrong-framework code looks plausible (it's React) but breaks the build or produces conflicting / routes at runtime.
import { Link, useNavigate, useSearchParams } from 'react-router-dom'
import Link from 'next/link'
import { useRouter, useParams } from 'next/navigation'
import {
Link,
Outlet,
useNavigate,
useRouter,
useLocation,
useParams,
redirect,
} from '@tanstack/react-router'
export const Route = createFileRoute('/posts')({
loader: async () => { ... },
validateSearch: zodValidator(schema),
component: PostsComponent,
})
const search = Route.useSearch()
If a build error mentions react-router-dom, next/, pages/_app, or duplicate / routes, fix the import — don't paper over with type assertions.
See also: router-core (Register setup), router-core/navigation (from narrowing), router-core/code-splitting (getRouteApi).