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.
| piece | where the type is born | how 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
| intent | pattern | why |
|---|---|---|
| declare params from path | createFileRoute('/users/$id') |
template-literal type parses $id → string |
| 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 |