Type Inference — how a route definition types everything

[check: …]
📖 guide (.md) ← react deep dive
📖 Pair this live simulator with the companion guide (.md) — this page is the rendered ground truth. ↗ Builds on frontend/tanstack-start: router type safety (the basics of type-safe routing — this bundle goes to how the type machinery works internally).

definition is the single source of truth

A route DEFINITION is where every type is born. TypeScript template-literal types parse the path string ('/users/$id'{'{ id: string }'}); validateSearch's return type becomes the search type; the loader's return type becomes useLoaderData()'s type. Those types are then registered into the library via declaration merging on the Register interface, so every consumer — <Link>, useNavigate, useParams — is checked against the exact route signature. You write fewer types; the router pipes them through for you.

piecewhere the type is bornhow it propagates
params path string: '/users/$id' template-literal type → {'{ id: string }'}
search validateSearch (zod / valibot / arktype) validator return type → search param type
loader data loader: ({params}) => fetchUser(...) return type → Route.useLoaderData()
context createRootRouteWithContext + beforeLoad accumulates down the tree → useRouteContext()
<Link> consumer (typed by the route) Register merges router types into the module

1 · the type-inference simulator (edit me)

routeTypes models the inferred type signatures. checkLink() is the simulated TS checker: it walks the route's param/search keys against the link's params/search and even validates literal-union values (e.g. tab must be 'posts' | 'comments').

2 · Babel compiles JSX → element tree

<TypeSim/> becomes React.createElement(TypeSim). The route registry + checker run as plain JS; React only re-renders the verdict when to/params/search change.

// (hit "compile & render" to see Babel's output)

3 · live type-inference simulator (proven)

Edit the route, params, or search above and watch the verdict flip. The gold-check drives it automatically: counts 4 routes, builds a valid link, then flips a param name to trigger a type error.

routes: · gold: 4 routes → valid link → param-name typo → type error

intent → pattern

intentpatternwhy
declare params from path createFileRoute('/users/$id') template-literal type parses $idstring
narrow a param type params: {{ parse: z.number() }} parsed parent type flows to every child route
declare search params validateSearch: zodValidator(z.object({...})) validator return type becomes the search type
consume in a component Route.useParams() / Route.useSearch() typed by THIS route's own signature (no from needed)
type-safe link <Link to="/users/$id" params={ {'id'} } search={ {'tab'} } /> checked against the Register-merged router types
register router types declare module {{ ' interface Register {{ router }}' }} declaration merging lets types cross the module boundary
shared component, unknown route useSearch({{ strict: false }}) relaxed but accurate union of all routes' search