@tanstack/router-core 1.167.3 → 1.167.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,497 @@
1
+ ---
2
+ name: router-core/type-safety
3
+ description: >-
4
+ Full type inference philosophy (never cast, never annotate inferred
5
+ values), Register module declaration, from narrowing on hooks and
6
+ Link, strict:false for shared components, getRouteApi for code-split
7
+ typed access, addChildren with object syntax for TS perf, LinkProps
8
+ and ValidateLinkOptions type utilities, as const satisfies pattern.
9
+ type: sub-skill
10
+ library: tanstack-router
11
+ library_version: '1.166.2'
12
+ requires:
13
+ - router-core
14
+ sources:
15
+ - TanStack/router:docs/router/guide/type-safety.md
16
+ - TanStack/router:docs/router/guide/type-utilities.md
17
+ - TanStack/router:docs/router/guide/render-optimizations.md
18
+ ---
19
+
20
+ # Type Safety
21
+
22
+ 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.
23
+
24
+ > **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.
25
+ > **CRITICAL**: Do NOT confuse TanStack Router with Next.js or React Router. There is no `getServerSideProps`, no `useSearchParams()`, no `useLoaderData()` from `react-router-dom`.
26
+
27
+ ## The ONE Required Type Annotation: Register
28
+
29
+ Without this, top-level exports like `Link`, `useNavigate`, `useSearch` have no type safety.
30
+
31
+ ```tsx
32
+ // src/router.tsx
33
+ import { createRouter } from '@tanstack/react-router'
34
+ import { routeTree } from './routeTree.gen'
35
+
36
+ const router = createRouter({ routeTree })
37
+
38
+ // THIS IS REQUIRED — the single type registration for the entire app
39
+ declare module '@tanstack/react-router' {
40
+ interface Register {
41
+ router: typeof router
42
+ }
43
+ }
44
+
45
+ export default router
46
+ ```
47
+
48
+ After registration, every `Link`, `useNavigate`, `useSearch`, `useParams` across the app is fully typed.
49
+
50
+ ## Types Flow Automatically
51
+
52
+ ### Route Hooks — No Annotation Needed
53
+
54
+ ```tsx
55
+ // src/routes/posts.$postId.tsx
56
+ import { createFileRoute } from '@tanstack/react-router'
57
+
58
+ export const Route = createFileRoute('/posts/$postId')({
59
+ validateSearch: (search: Record<string, unknown>) => ({
60
+ page: Number(search.page ?? 1),
61
+ }),
62
+ loader: async ({ params }) => {
63
+ // params.postId is already typed as string — do not annotate
64
+ const post = await fetchPost(params.postId)
65
+ return { post }
66
+ },
67
+ component: PostComponent,
68
+ })
69
+
70
+ function PostComponent() {
71
+ // ALL of these are fully inferred — do NOT add type annotations
72
+ const { postId } = Route.useParams()
73
+ // ^? string
74
+
75
+ const { page } = Route.useSearch()
76
+ // ^? number
77
+
78
+ const { post } = Route.useLoaderData()
79
+ // ^? { id: string; title: string; body: string }
80
+
81
+ return (
82
+ <div>
83
+ <h1>{post.title}</h1>
84
+ <p>Page {page}</p>
85
+ </div>
86
+ )
87
+ }
88
+ ```
89
+
90
+ ### Context Flows Through the Tree
91
+
92
+ ```tsx
93
+ // src/routes/__root.tsx
94
+ import { createRootRouteWithContext, Outlet } from '@tanstack/react-router'
95
+
96
+ interface RouterContext {
97
+ auth: { userId: string; role: 'admin' | 'user' } | null
98
+ }
99
+
100
+ // Note: createRootRouteWithContext is a FACTORY — call it TWICE: ()()
101
+ export const Route = createRootRouteWithContext<RouterContext>()({
102
+ component: () => <Outlet />,
103
+ })
104
+ ```
105
+
106
+ ```tsx
107
+ // src/routes/dashboard.tsx
108
+ import { createFileRoute, redirect } from '@tanstack/react-router'
109
+
110
+ export const Route = createFileRoute('/dashboard')({
111
+ beforeLoad: ({ context }) => {
112
+ // context.auth is already typed as { userId: string; role: 'admin' | 'user' } | null
113
+ // NO annotation needed
114
+ if (!context.auth) throw redirect({ to: '/login' })
115
+ return { user: context.auth }
116
+ },
117
+ loader: ({ context }) => {
118
+ // context.user is typed as { userId: string; role: 'admin' | 'user' }
119
+ // This was added by beforeLoad above — fully inferred
120
+ return fetchDashboard(context.user.userId)
121
+ },
122
+ component: DashboardComponent,
123
+ })
124
+
125
+ function DashboardComponent() {
126
+ const data = Route.useLoaderData()
127
+ const { user } = Route.useRouteContext()
128
+ return <h1>Welcome {user.userId}</h1>
129
+ }
130
+ ```
131
+
132
+ ## Narrowing with `from`
133
+
134
+ Without `from`, hooks return a union of ALL routes' types — slow for TypeScript and imprecise.
135
+
136
+ ### On Hooks
137
+
138
+ ```tsx
139
+ import { useSearch, useParams, useNavigate } from '@tanstack/react-router'
140
+
141
+ function PostSidebar() {
142
+ // WRONG — search is a union of ALL routes' search params
143
+ const search = useSearch()
144
+
145
+ // CORRECT — search is narrowed to /posts/$postId's search params
146
+ const search = useSearch({ from: '/posts/$postId' })
147
+ // ^? { page: number }
148
+
149
+ // CORRECT — params narrowed to this route
150
+ const { postId } = useParams({ from: '/posts/$postId' })
151
+
152
+ // CORRECT — navigate narrowed for relative paths
153
+ const navigate = useNavigate({ from: '/posts/$postId' })
154
+ }
155
+ ```
156
+
157
+ ### On `Link`
158
+
159
+ ```tsx
160
+ import { Link } from '@tanstack/react-router'
161
+
162
+ // WRONG — search resolves to union of ALL routes' search params, slow TS check
163
+ <Link to=".." search={{ page: 0 }} />
164
+
165
+ // CORRECT — narrowed, fast TS check
166
+ <Link from="/posts/$postId" to=".." search={{ page: 0 }} />
167
+
168
+ // Also correct — Route.fullPath in route components
169
+ <Link from={Route.fullPath} to=".." search={{ page: 0 }} />
170
+ ```
171
+
172
+ ## Shared Components: `strict: false`
173
+
174
+ When a component is used across multiple routes, use `strict: false` instead of `from`:
175
+
176
+ ```tsx
177
+ import { useSearch } from '@tanstack/react-router'
178
+
179
+ function GlobalSearch() {
180
+ // Returns union of all routes' search params — no runtime error if route doesn't match
181
+ const search = useSearch({ strict: false })
182
+ return <span>Query: {search.q ?? ''}</span>
183
+ }
184
+ ```
185
+
186
+ ## Code-Split Files: `getRouteApi`
187
+
188
+ Use `getRouteApi` instead of importing `Route` to avoid pulling route config into the lazy chunk:
189
+
190
+ ```tsx
191
+ // src/routes/posts.lazy.tsx
192
+ import { createLazyFileRoute, getRouteApi } from '@tanstack/react-router'
193
+
194
+ const routeApi = getRouteApi('/posts')
195
+
196
+ export const Route = createLazyFileRoute('/posts')({
197
+ component: PostsComponent,
198
+ })
199
+
200
+ function PostsComponent() {
201
+ const data = routeApi.useLoaderData()
202
+ const { page } = routeApi.useSearch()
203
+ return <div>Page {page}</div>
204
+ }
205
+ ```
206
+
207
+ ## TypeScript Performance
208
+
209
+ ### Use Object Syntax for `addChildren` in Large Route Trees
210
+
211
+ ```tsx
212
+ // SLOWER — tuple syntax
213
+ const routeTree = rootRoute.addChildren([
214
+ postsRoute.addChildren([postRoute, postsIndexRoute]),
215
+ indexRoute,
216
+ ])
217
+
218
+ // FASTER — object syntax (TS checks objects faster than large tuples)
219
+ const routeTree = rootRoute.addChildren({
220
+ postsRoute: postsRoute.addChildren({ postRoute, postsIndexRoute }),
221
+ indexRoute,
222
+ })
223
+ ```
224
+
225
+ With file-based routing the route tree is generated, so this is handled for you.
226
+
227
+ ### Avoid Returning Unused Inferred Types from Loaders
228
+
229
+ When using external caches like TanStack Query, don't let the router infer complex return types you never consume:
230
+
231
+ ```tsx
232
+ // SLOWER — TS infers the full ensureQueryData return type into the route tree
233
+ export const Route = createFileRoute('/posts/$postId')({
234
+ loader: ({ context: { queryClient }, params: { postId } }) =>
235
+ queryClient.ensureQueryData(postQueryOptions(postId)),
236
+ component: PostComponent,
237
+ })
238
+
239
+ // FASTER — void return, inference stays out of the route tree
240
+ export const Route = createFileRoute('/posts/$postId')({
241
+ loader: async ({ context: { queryClient }, params: { postId } }) => {
242
+ await queryClient.ensureQueryData(postQueryOptions(postId))
243
+ },
244
+ component: PostComponent,
245
+ })
246
+ ```
247
+
248
+ ### `as const satisfies` for Link Option Objects
249
+
250
+ Never use `LinkProps` as a variable type — it's an enormous union:
251
+
252
+ ```tsx
253
+ import type { LinkProps, RegisteredRouter } from '@tanstack/react-router'
254
+
255
+ // WRONG — LinkProps is a massive union, extremely slow TS check
256
+ const wrongProps: LinkProps = { to: '/posts' }
257
+
258
+ // CORRECT — infer a precise type, validate against LinkProps
259
+ const goodProps = { to: '/posts' } as const satisfies LinkProps
260
+
261
+ // EVEN BETTER — narrow LinkProps with generic params
262
+ const narrowedProps = {
263
+ to: '/posts',
264
+ } as const satisfies LinkProps<RegisteredRouter, string, '/posts'>
265
+ ```
266
+
267
+ ### Type-Safe Link Option Arrays
268
+
269
+ ```tsx
270
+ import type { LinkProps } from '@tanstack/react-router'
271
+
272
+ export const navLinks = [
273
+ { to: '/posts' },
274
+ { to: '/posts/$postId', params: { postId: '1' } },
275
+ ] as const satisfies ReadonlyArray<LinkProps>
276
+
277
+ // Use the precise inferred type, not LinkProps directly
278
+ export type NavLink = (typeof navLinks)[number]
279
+ ```
280
+
281
+ ## Type Utilities for Generic Components
282
+
283
+ ### `ValidateLinkOptions` — Type-Safe Link Props in Custom Components
284
+
285
+ ```tsx
286
+ import {
287
+ Link,
288
+ type RegisteredRouter,
289
+ type ValidateLinkOptions,
290
+ } from '@tanstack/react-router'
291
+
292
+ interface NavItemProps<
293
+ TRouter extends RegisteredRouter = RegisteredRouter,
294
+ TOptions = unknown,
295
+ > {
296
+ label: string
297
+ linkOptions: ValidateLinkOptions<TRouter, TOptions>
298
+ }
299
+
300
+ export function NavItem<TRouter extends RegisteredRouter, TOptions>(
301
+ props: NavItemProps<TRouter, TOptions>,
302
+ ): React.ReactNode
303
+ export function NavItem(props: NavItemProps): React.ReactNode {
304
+ return (
305
+ <li>
306
+ <Link {...props.linkOptions}>{props.label}</Link>
307
+ </li>
308
+ )
309
+ }
310
+
311
+ // Usage — fully type-safe
312
+ <NavItem label="Posts" linkOptions={{ to: '/posts' }} />
313
+ <NavItem label="Post" linkOptions={{ to: '/posts/$postId', params: { postId: '1' } }} />
314
+ ```
315
+
316
+ ### `ValidateNavigateOptions` — Type-Safe Navigate in Utilities
317
+
318
+ ```tsx
319
+ import {
320
+ useNavigate,
321
+ type RegisteredRouter,
322
+ type ValidateNavigateOptions,
323
+ } from '@tanstack/react-router'
324
+
325
+ export function useDelayedNavigate<
326
+ TRouter extends RegisteredRouter = RegisteredRouter,
327
+ TOptions = unknown,
328
+ >(
329
+ options: ValidateNavigateOptions<TRouter, TOptions>,
330
+ delayMs: number,
331
+ ): () => void
332
+ export function useDelayedNavigate(
333
+ options: ValidateNavigateOptions,
334
+ delayMs: number,
335
+ ): () => void {
336
+ const navigate = useNavigate()
337
+ return () => {
338
+ setTimeout(() => navigate(options), delayMs)
339
+ }
340
+ }
341
+
342
+ // Usage — type-safe
343
+ const go = useDelayedNavigate(
344
+ { to: '/posts/$postId', params: { postId: '1' } },
345
+ 500,
346
+ )
347
+ ```
348
+
349
+ ### `ValidateRedirectOptions` — Type-Safe Redirect in Utilities
350
+
351
+ ```tsx
352
+ import {
353
+ redirect,
354
+ type RegisteredRouter,
355
+ type ValidateRedirectOptions,
356
+ } from '@tanstack/react-router'
357
+
358
+ export async function fetchOrRedirect<
359
+ TRouter extends RegisteredRouter = RegisteredRouter,
360
+ TOptions = unknown,
361
+ >(
362
+ url: string,
363
+ redirectOptions: ValidateRedirectOptions<TRouter, TOptions>,
364
+ ): Promise<unknown>
365
+ export async function fetchOrRedirect(
366
+ url: string,
367
+ redirectOptions: ValidateRedirectOptions,
368
+ ): Promise<unknown> {
369
+ const response = await fetch(url)
370
+ if (!response.ok && response.status === 401) throw redirect(redirectOptions)
371
+ return response.json()
372
+ }
373
+ ```
374
+
375
+ ### Render Props for Maximum Performance
376
+
377
+ Instead of accepting `LinkProps`, invert control so `Link` is narrowed at the call site:
378
+
379
+ ```tsx
380
+ function Card(props: { title: string; renderLink: () => React.ReactNode }) {
381
+ return (
382
+ <div>
383
+ <h2>{props.title}</h2>
384
+ {props.renderLink()}
385
+ </div>
386
+ )
387
+ }
388
+
389
+ // Link narrowed to exactly /posts — no union check
390
+ ;<Card title="All Posts" renderLink={() => <Link to="/posts">View</Link>} />
391
+ ```
392
+
393
+ ## Render Optimizations
394
+
395
+ ### Fine-Grained Selectors with `select`
396
+
397
+ ```tsx
398
+ function PostTitle() {
399
+ // Only re-renders when page changes, not when other search params change
400
+ const page = Route.useSearch({ select: ({ page }) => page })
401
+ return <span>Page {page}</span>
402
+ }
403
+ ```
404
+
405
+ ### Structural Sharing
406
+
407
+ Preserve referential identity across re-renders for search params:
408
+
409
+ ```tsx
410
+ const router = createRouter({
411
+ routeTree,
412
+ defaultStructuralSharing: true, // Enable globally
413
+ })
414
+
415
+ // Or per-hook
416
+ const result = Route.useSearch({
417
+ select: (search) => ({ foo: search.foo, label: `Page ${search.foo}` }),
418
+ structuralSharing: true,
419
+ })
420
+ ```
421
+
422
+ Structural sharing only works with JSON-compatible data. TypeScript will error if you return class instances with `structuralSharing: true`.
423
+
424
+ ## Common Mistakes
425
+
426
+ ### 1. CRITICAL: Adding type annotations or casts to inferred values
427
+
428
+ ```tsx
429
+ // WRONG — casting masks real type errors
430
+ const search = useSearch({ from: '/posts' }) as { page: number }
431
+
432
+ // WRONG — unnecessary annotation
433
+ const params: { postId: string } = useParams({ from: '/posts/$postId' })
434
+
435
+ // WRONG — generic param on hook
436
+ const data = useLoaderData<{ posts: Post[] }>({ from: '/posts' })
437
+
438
+ // CORRECT — let inference work
439
+ const search = useSearch({ from: '/posts' })
440
+ const params = useParams({ from: '/posts/$postId' })
441
+ const data = useLoaderData({ from: '/posts' })
442
+ ```
443
+
444
+ ### 2. HIGH: Using un-narrowed `LinkProps` type
445
+
446
+ ```tsx
447
+ // WRONG — LinkProps is a massive union, causes severe TS slowdown
448
+ const myProps: LinkProps = { to: '/posts' }
449
+
450
+ // CORRECT — use as const satisfies for precise inference
451
+ const myProps = { to: '/posts' } as const satisfies LinkProps
452
+ ```
453
+
454
+ ### 3. HIGH: Not narrowing `Link`/`useNavigate` with `from`
455
+
456
+ ```tsx
457
+ // WRONG — search is a union of ALL routes, TS check grows with route count
458
+ <Link to=".." search={{ page: 0 }} />
459
+
460
+ // CORRECT — narrowed, fast check
461
+ <Link from={Route.fullPath} to=".." search={{ page: 0 }} />
462
+ ```
463
+
464
+ ### 4. CRITICAL (cross-skill): Missing router type registration
465
+
466
+ ```tsx
467
+ // WRONG — Link/useNavigate have no autocomplete, all paths are untyped strings
468
+ const router = createRouter({ routeTree })
469
+ // (no declare module)
470
+
471
+ // CORRECT — always register
472
+ const router = createRouter({ routeTree })
473
+ declare module '@tanstack/react-router' {
474
+ interface Register {
475
+ router: typeof router
476
+ }
477
+ }
478
+ ```
479
+
480
+ ### 5. CRITICAL (cross-skill): Generating Next.js or Remix patterns
481
+
482
+ ```tsx
483
+ // WRONG — these are NOT TanStack Router APIs
484
+ export async function getServerSideProps() { ... }
485
+ export async function loader({ request }) { ... } // Remix-style
486
+ const [searchParams, setSearchParams] = useSearchParams() // React Router
487
+
488
+ // CORRECT — TanStack Router APIs
489
+ export const Route = createFileRoute('/posts')({
490
+ loader: async () => { ... }, // TanStack loader
491
+ validateSearch: zodValidator(schema), // TanStack search validation
492
+ component: PostsComponent,
493
+ })
494
+ const search = Route.useSearch() // TanStack hook
495
+ ```
496
+
497
+ See also: router-core (Register setup), router-core/navigation (from narrowing), router-core/code-splitting (getRouteApi).