Repository: TanStack/router
Pull request #1729 (2024-06-09 22:38:22)

perf(react-router): remove root search schema type and unused type parameters from route options
- remove root search schema as it doesn't seem to be needed
- remove unused type parameters for route options
- added tests to cover index routes and root search schema

nx-cloud[bot]: # ☁️ Nx Cloud Report

CI is running for commit 93d1b13f7e93a93b30fdf65361d4f53dd4acd844.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/66662ef8038c541b009c7368?utm_source=pull-request&utm_medium=comment)


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
chorobin: Reduces instantiations a bit because we don't need to exclude/omit `RootSearchSchema`
Before
```
> tsc --extendedDiagnostics

Files:                         644
Lines of Library:            38411
Lines of Definitions:        82809
Lines of TypeScript:         19639
Lines of JavaScript:             0
Lines of JSON:                   0
Lines of Other:                  0
Identifiers:                134746
Symbols:                    326768
Types:                      115290
Instantiations:             826289
Memory used:               392053K
Assignability cache size:    47858
Identity cache size:          8641
Subtype cache size:           2426
Strict subtype cache size:     413
I/O Read time:               0.10s
Parse time:                  0.78s
ResolveModule time:          0.16s
ResolveTypeReference time:   0.01s
ResolveLibrary time:         0.02s
Program time:                1.19s
Bind time:                   0.32s
Check time:                  5.96s
printTime time:              0.00s
Emit time:                   0.00s
Total time:                  7.47s
```

```
> tsc --extendedDiagnostics

Files:                         644
Lines of Library:            38411
Lines of Definitions:        82800
Lines of TypeScript:         19639
Lines of JavaScript:             0
Lines of JSON:                   0
Lines of Other:                  0
Identifiers:                134667
Symbols:                    326556
Types:                      115064
Instantiations:             816475
Memory used:               386904K
Assignability cache size:    47482
Identity cache size:          8539
Subtype cache size:           2426
Strict subtype cache size:     413
I/O Read time:               0.08s
Parse time:                  0.59s
ResolveModule time:          0.13s
ResolveTypeReference time:   0.00s
ResolveLibrary time:         0.02s
Program time:                0.92s
Bind time:                   0.27s
Check time:                  5.47s
printTime time:              0.00s
Emit time:                   0.00s
Total time:                  6.65s
```
chorobin: Don't merge just yet. Just want to double check everything
SeanCassiere: Just want to confirm the `RootSearchSchema` type wasn't being used for Router inference right? So it's getting nuked.
chorobin: > Just want to confirm the `RootSearchSchema` type wasn't being used for Router inference right? So it's getting nuked.

From what I understand from conversations with @schiller-manuel  the users don't use this type unlike `SearchSchemaInput`?  It seems like there was s bug some time ago confusing the root route and the index route. But this doesn't seem to be a problem anymore. There's been a lot of changes to the types since then so if we don't need it, I'd remove it because it just means we need to check for it all the time

Patch:
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index 47f151341..663e11ce4 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -156,7 +156,6 @@ export {
   type AnyRoute,
   type RouteConstraints,
   type AnyRootRoute,
-  type RootSearchSchema,
   type ResolveFullPath,
   type RouteMask,
   type ErrorRouteProps,
diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx
index 0783dce0a..4f91206cb 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -7,7 +7,6 @@ import { deepEqual, functionalUpdate } from './utils'
 import { exactPathTest, removeTrailingSlash } from './path'
 import type { AnyRouter, ParsedLocation } from '.'
 import type { HistoryState } from '@tanstack/history'
-import type { AnyRoute, RootSearchSchema } from './route'
 import type {
   AllParams,
   CatchAllPaths,
@@ -251,9 +250,6 @@ type ParamsReducer<
 
 type ParamVariant = 'PATH' | 'SEARCH'
 
-type ExcludeRootSearchSchema<T> =
-  Exclude<T, RootSearchSchema> extends never ? {} : Exclude<T, RootSearchSchema>
-
 export type ResolveRoute<
   TRouter extends AnyRouter,
   TFrom,
@@ -265,11 +261,6 @@ export type ResolveRoute<
     : RouteByToPath<TRouter, TPath>
   : never
 
-type PostProcessParams<
-  T,
-  TParamVariant extends ParamVariant,
-> = TParamVariant extends 'SEARCH' ? ExcludeRootSearchSchema<T> : T
-
 type ResolveFromParamType<TParamVariant extends ParamVariant> =
   TParamVariant extends 'PATH' ? 'allParams' : 'fullSearchSchema'
 
@@ -312,14 +303,11 @@ export type ResolveToParams<
       ? ResolveAllToParams<TRouter, TParamVariant>
       : TPath extends CatchAllPaths
         ? ResolveAllToParams<TRouter, TParamVariant>
-        : PostProcessParams<
-            ResolveRoute<
-              TRouter,
-              TFrom,
-              TTo
-            >['types'][ResolveToParamType<TParamVariant>],
-            TParamVariant
-          >
+        : ResolveRoute<
+            TRouter,
+            TFrom,
+            TTo
+          >['types'][ResolveToParamType<TParamVariant>]
     : never
 
 type ResolveRelativeToParams<
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index e6170f533..7b32a9b62 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -56,8 +56,6 @@ export type RouteOptions<
   TPath extends string = string,
   TSearchSchemaInput = Record<string, unknown>,
   TSearchSchema = {},
-  TSearchSchemaUsed = {},
-  TFullSearchSchemaInput = TSearchSchemaUsed,
   TFullSearchSchema = TSearchSchema,
   TParams = AnyPathParams,
   TAllParams = TParams,
@@ -74,8 +72,6 @@ export type RouteOptions<
   TPath,
   TSearchSchemaInput,
   TSearchSchema,
-  TSearchSchemaUsed,
-  TFullSearchSchemaInput,
   TFullSearchSchema,
   TParams,
   TAllParams,
@@ -170,8 +166,6 @@ export type BaseRouteOptions<
   TPath extends string = string,
   TSearchSchemaInput = Record<string, unknown>,
   TSearchSchema = {},
-  TSearchSchemaUsed = {},
-  TFullSearchSchemaInput = TSearchSchemaUsed,
   TFullSearchSchema = TSearchSchema,
   TParams = {},
   TAllParams = ParamsFallback<TPath, TParams>,
@@ -398,28 +392,15 @@ export type ResolveFullSearchSchema<
   TParentRoute extends AnyRoute,
   TSearchSchema,
 > = unknown extends TParentRoute
-  ? Omit<TSearchSchema, keyof RootSearchSchema>
-  : Assign<
-      TParentRoute['id'] extends RootRouteId
-        ? Omit<TParentRoute['types']['searchSchema'], keyof RootSearchSchema>
-        : TParentRoute['types']['fullSearchSchema'],
-      TSearchSchema
-    >
+  ? TSearchSchema
+  : Assign<TParentRoute['types']['fullSearchSchema'], TSearchSchema>
 
 export type ResolveFullSearchSchemaInput<
   TParentRoute extends AnyRoute,
   TSearchSchemaUsed,
 > = unknown extends TParentRoute
-  ? Omit<TSearchSchemaUsed, keyof RootSearchSchema>
-  : Assign<
-      TParentRoute['id'] extends RootRouteId
-        ? Omit<
-            TParentRoute['types']['searchSchemaInput'],
-            keyof RootSearchSchema
-          >
-        : TParentRoute['types']['fullSearchSchemaInput'],
-      TSearchSchemaUsed
-    >
+  ? TSearchSchemaUsed
+  : Assign<TParentRoute['types']['fullSearchSchemaInput'], TSearchSchemaUsed>
 
 export type ResolveRouteContext<TRouteContextReturn> = [
   TRouteContextReturn,
@@ -628,8 +609,6 @@ export class Route<
     TPath,
     TSearchSchemaInput,
     TSearchSchema,
-    TSearchSchemaUsed,
-    TFullSearchSchemaInput,
     TFullSearchSchema,
     TParams,
     TAllParams,
@@ -667,8 +646,6 @@ export class Route<
       TPath,
       TSearchSchemaInput,
       TSearchSchema,
-      TSearchSchemaUsed,
-      TFullSearchSchemaInput,
       TFullSearchSchema,
       TParams,
       TAllParams,
@@ -723,8 +700,6 @@ export class Route<
           TPath,
           TSearchSchemaInput,
           TSearchSchema,
-          TSearchSchemaUsed,
-          TFullSearchSchemaInput,
           TFullSearchSchema,
           TParams,
           TAllParams,
@@ -969,8 +944,6 @@ export function createRoute<
     TPath,
     TSearchSchemaInput,
     TSearchSchema,
-    TSearchSchemaUsed,
-    TFullSearchSchemaInput,
     TFullSearchSchema,
     TParams,
     TAllParams,
@@ -1010,9 +983,8 @@ export function createRoute<
 export type AnyRootRoute = RootRoute<any, any, any, any, any, any, any, any>
 
 export type RootRouteOptions<
-  TSearchSchemaInput = RootSearchSchema,
-  TSearchSchema = RootSearchSchema,
-  TSearchSchemaUsed = RootSearchSchema,
+  TSearchSchemaInput = {},
+  TSearchSchema = {},
   TRouteContextReturn = RouteContext,
   TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   TRouterContext = {},
@@ -1026,8 +998,6 @@ export type RootRouteOptions<
     '', // TPath
     TSearchSchemaInput, // TSearchSchemaInput
     TSearchSchema, // TSearchSchema
-    TSearchSchemaUsed,
-    TSearchSchemaUsed, //TFullSearchSchemaInput
     TSearchSchema, // TFullSearchSchema
     {}, // TParams
     {}, // TAllParams
@@ -1049,13 +1019,15 @@ export type RootRouteOptions<
 
 export function createRootRouteWithContext<TRouterContext extends {}>() {
   return <
-    TSearchSchemaInput extends Record<string, any> = RootSearchSchema,
-    TSearchSchema extends Record<string, any> = RootSearchSchema,
-    TSearchSchemaUsed extends Record<string, any> = RootSearchSchema,
+    TSearchSchemaInput = {},
+    TSearchSchema = {},
+    TSearchSchemaUsed = ResolveSearchSchemaUsed<
+      TSearchSchemaInput,
+      TSearchSchema
+    >,
     TRouteContextReturn extends RouteContext = RouteContext,
-    TRouteContext extends RouteContext = [TRouteContextReturn] extends [never]
-      ? RouteContext
-      : TRouteContextReturn,
+    TRouteContext extends
+      RouteContext = ResolveRouteContext<TRouteContextReturn>,
     TLoaderDeps extends Record<string, any> = {},
     TLoaderDataReturn = {},
     TLoaderData = ResolveLoaderData<TLoaderDataReturn>,
@@ -1063,7 +1035,6 @@ export function createRootRouteWithContext<TRouterContext extends {}>() {
     options?: RootRouteOptions<
       TSearchSchemaInput,
       TSearchSchema,
-      TSearchSchemaUsed,
       TRouteContextReturn,
       TRouteContext,
       TRouterContext,
@@ -1090,14 +1061,10 @@ export function createRootRouteWithContext<TRouterContext extends {}>() {
  */
 export const rootRouteWithContext = createRootRouteWithContext
 
-export type RootSearchSchema = {
-  __TRootSearchSchema__: '__TRootSearchSchema__'
-}
-
 export class RootRoute<
-  in out TSearchSchemaInput = RootSearchSchema,
-  in out TSearchSchema = RootSearchSchema,
-  in out TSearchSchemaUsed = RootSearchSchema,
+  in out TSearchSchemaInput = {},
+  in out TSearchSchema = {},
+  in out TSearchSchemaUsed = {},
   TRouteContextReturn = RouteContext,
   in out TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   in out TRouterContext = {},
@@ -1133,7 +1100,6 @@ export class RootRoute<
     options?: RootRouteOptions<
       TSearchSchemaInput,
       TSearchSchema,
-      TSearchSchemaUsed,
       TRouteContextReturn,
       TRouteContext,
       TRouterContext,
@@ -1147,9 +1113,12 @@ export class RootRoute<
 }
 
 export function createRootRoute<
-  TSearchSchemaInput = RootSearchSchema,
-  TSearchSchema = RootSearchSchema,
-  TSearchSchemaUsed = RootSearchSchema,
+  TSearchSchemaInput = {},
+  TSearchSchema = {},
+  TSearchSchemaUsed = ResolveSearchSchemaUsed<
+    TSearchSchemaInput,
+    TSearchSchema
+  >,
   TRouteContextReturn = RouteContext,
   TRouteContext = ResolveRouteContext<TRouteContextReturn>,
   TRouterContext = {},
@@ -1164,9 +1133,7 @@ export function createRootRoute<
       '', // TPath
       TSearchSchemaInput, // TSearchSchemaInput
       TSearchSchema, // TSearchSchema
-      TSearchSchemaUsed,
-      TSearchSchemaUsed, // TFullSearchSchemaInput
-      TSearchSchema, // TFullSearchSchema
+      TSearchSchema,
       {}, // TParams
       {}, // TAllParams
       TRouteContextReturn, // TRouteContextReturn
@@ -1306,10 +1273,7 @@ export class NotFoundRoute<
   TFullSearchSchema = ResolveFullSearchSchema<TParentRoute, TSearchSchema>,
   TRouteContextReturn = AnyContext,
   TRouteContext = RouteContext,
-  TAllContext = Assign<
-    IsAny<TParentRoute['types']['allContext'], {}>,
-    TRouteContext
-  >,
+  TAllContext = ResolveAllContext<TParentRoute, TRouteContext>,
   TRouterContext = AnyContext,
   TLoaderDeps extends Record<string, any> = {},
   TLoaderDataReturn = {},
@@ -1345,8 +1309,6 @@ export class NotFoundRoute<
         string,
         TSearchSchemaInput,
         TSearchSchema,
-        TSearchSchemaUsed,
-        TFullSearchSchemaInput,
         TFullSearchSchema,
         {},
         {},
diff --git a/packages/react-router/src/routeInfo.ts b/packages/react-router/src/routeInfo.ts
index f9d5f51b7..fea4d0219 100644
--- a/packages/react-router/src/routeInfo.ts
+++ b/packages/react-router/src/routeInfo.ts
@@ -1,4 +1,4 @@
-import type { AnyRoute, RootSearchSchema } from './route'
+import type { AnyRoute } from './route'
 import type { AnyRouter, TrailingSlashOption } from './router'
 import type { Expand, MergeUnion } from './utils'
 
@@ -115,14 +115,11 @@ export type RouteByToPath<TRouter extends AnyRouter, TTo> = Extract<
 >
 
 export type FullSearchSchema<TRouteTree extends AnyRoute> = MergeUnion<
-  Exclude<ParseRoute<TRouteTree>['types']['fullSearchSchema'], RootSearchSchema>
+  ParseRoute<TRouteTree>['types']['fullSearchSchema']
 >
 
 export type FullSearchSchemaInput<TRouteTree extends AnyRoute> = MergeUnion<
-  Exclude<
-    ParseRoute<TRouteTree>['types']['fullSearchSchemaInput'],
-    RootSearchSchema
-  >
+  ParseRoute<TRouteTree>['types']['fullSearchSchemaInput']
 >
 
 export type AllParams<TRouteTree extends AnyRoute> = MergeUnion<
diff --git a/packages/react-router/src/useSearch.tsx b/packages/react-router/src/useSearch.tsx
index 4d2fc778c..d4dbcebcb 100644
--- a/packages/react-router/src/useSearch.tsx
+++ b/packages/react-router/src/useSearch.tsx
@@ -1,9 +1,9 @@
 import { useMatch } from './useMatch'
-import type { AnyRoute, RootSearchSchema } from './route'
+import type { AnyRoute } from './route'
 import type { FullSearchSchema, RouteById, RouteIds } from './routeInfo'
 import type { RegisteredRouter } from './router'
 import type { MakeRouteMatch } from './Matches'
-import type { StrictOrFrom } from './utils'
+import type { Expand, StrictOrFrom } from './utils'
 
 export type UseSearchOptions<
   TFrom,
@@ -20,10 +20,7 @@ export function useSearch<
   TStrict extends boolean = true,
   TSearch = TStrict extends false
     ? FullSearchSchema<TRouteTree>
-    : Exclude<
-        RouteById<TRouteTree, TFrom>['types']['fullSearchSchema'],
-        RootSearchSchema
-      >,
+    : Expand<RouteById<TRouteTree, TFrom>['types']['fullSearchSchema']>,
   TSelected = TSearch,
 >(opts: UseSearchOptions<TFrom, TStrict, TSearch, TSelected>): TSelected {
   return useMatch({


Test patch:
diff --git a/packages/react-router/tests/link.test-d.tsx b/packages/react-router/tests/link.test-d.tsx
index 98724329e..1aa58f82b 100644
--- a/packages/react-router/tests/link.test-d.tsx
+++ b/packages/react-router/tests/link.test-d.tsx
@@ -2,14 +2,17 @@ import { expectTypeOf, test } from 'vitest'
 import { Link, createRoute, createRouter } from '../src'
 import { createRootRoute } from '../src'
 
-const rootRoute = createRootRoute()
+const rootRoute = createRootRoute({
+  validateSearch: (): { rootPage?: number } => ({ rootPage: 0 }),
+})
 
 const indexRoute = createRoute({
   getParentRoute: () => rootRoute,
   path: '/',
+  validateSearch: () => ({ rootIndexPage: 0 }),
 })
 
-export const postsRoute = createRoute({
+const postsRoute = createRoute({
   getParentRoute: () => rootRoute,
   path: 'posts',
 })
@@ -114,7 +117,25 @@ type RouterNeverTrailingSlashes = typeof routerNeverTrailingSlashes
 type RouterPreserveTrailingSlashes = typeof routerPreserveTrailingSlashes
 
 test('when navigating to the root', () => {
-  expectTypeOf(Link<DefaultRouter, string, '/'>)
+  const DefaultRouterLink = Link<DefaultRouter, string, '/'>
+  const DefaultRouterObjectsLink = Link<DefaultRouterObjects, string, '/'>
+  const RouterAlwaysTrailingSlashLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '/'
+  >
+  const RouterNeverTrailingSlashLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '/'
+  >
+  const RouterPreserveTrailingSlashLink = Link<
+    RouterPreserveTrailingSlashes,
+    string,
+    '/'
+  >
+
+  expectTypeOf(DefaultRouterLink)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
@@ -131,7 +152,7 @@ test('when navigating to the root', () => {
       | undefined
     >()
 
-  expectTypeOf(Link<DefaultRouterObjects, string, '/'>)
+  expectTypeOf(DefaultRouterObjectsLink)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
@@ -148,7 +169,7 @@ test('when navigating to the root', () => {
       | undefined
     >()
 
-  expectTypeOf(Link<RouterAlwaysTrailingSlashes, string, '/'>)
+  expectTypeOf(RouterAlwaysTrailingSlashLink)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
@@ -165,7 +186,7 @@ test('when navigating to the root', () => {
       | undefined
     >()
 
-  expectTypeOf(Link<RouterNeverTrailingSlashes, string, '/'>)
+  expectTypeOf(RouterNeverTrailingSlashLink)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
@@ -182,7 +203,7 @@ test('when navigating to the root', () => {
       | undefined
     >()
 
-  expectTypeOf(Link<RouterPreserveTrailingSlashes, string, '/'>)
+  expectTypeOf(RouterPreserveTrailingSlashLink)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
@@ -205,6 +226,131 @@ test('when navigating to the root', () => {
       | '/posts/$postId/'
       | undefined
     >()
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(DefaultRouterObjectsLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(DefaultRouterObjectsLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(RouterNeverTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{
+      page?: number
+      rootIndexPage?: number
+      rootPage?: number
+    }>()
+
+  expectTypeOf(DefaultRouterObjectsLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{
+      page?: number
+      rootIndexPage?: number
+      rootPage?: number
+    }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{
+      page?: number
+      rootIndexPage?: number
+      rootPage?: number
+    }>()
+
+  expectTypeOf(RouterNeverTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{
+      page?: number
+      rootIndexPage?: number
+      rootPage?: number
+    }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<{
+      page?: number
+      rootIndexPage?: number
+      rootPage?: number
+    }>()
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(DefaultRouterObjectsLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(RouterNeverTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number; rootIndexPage: number }>()
 })
 
 test('when navigating from a route with no params and no search to the root', () => {
@@ -2041,53 +2187,78 @@ test('when navigating to a route with search params', () => {
 
   defaultRouterLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+    .toEqualTypeOf<{ rootPage?: number; page: number }>()
 
   defaultRouterObjectsLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+    .toEqualTypeOf<{ rootPage?: number; page: number }>()
 
   routerAlwaysTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+    .toEqualTypeOf<{ rootPage?: number; page: number }>()
 
   routerNeverTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+    .toEqualTypeOf<{ rootPage?: number; page: number }>()
 
   routerPreserveTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+    .toEqualTypeOf<{ rootPage?: number; page: number }>()
 
-  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page: number }>()
+  defaultRouterLinkSearch.returns.toEqualTypeOf<{
+    page: number
+    rootPage?: number
+  }>()
 
-  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<{ page: number }>()
+  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<{
+    page: number
+    rootPage?: number
+  }>()
 
   routerAlwaysTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
     page: number
+    rootPage?: number
   }>()
 
-  routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<{ page: number }>()
+  routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
+    page: number
+  }>()
 
   routerPreserveTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
     page: number
+    rootPage?: number
   }>()
 
-  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{ page?: number }>()
+  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{ page?: number }>()
+  defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerAlwaysTrailingSlashesLinkSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerAlwaysTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerNeverTrailingSlashesLinkSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerNeverTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerPreserveTrailingSlashesLinkSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerPreserveTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 })
 
 test('when navigating to a route with optional search params', () => {
@@ -2168,57 +2339,106 @@ test('when navigating to a route with optional search params', () => {
     .parameter(0)
     .not.toMatchTypeOf<{ search: unknown }>()
 
-  defaultRouterLinkSearch
-    .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+  defaultRouterLinkSearch.exclude<Function | boolean>().toEqualTypeOf<
+    | {
+        rootPage?: number
+        page?: number
+      }
+    | undefined
+  >()
 
-  defaultRouterObjectsLinkSearch
-    .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+  defaultRouterObjectsLinkSearch.exclude<Function | boolean>().toEqualTypeOf<
+    | {
+        rootPage?: number
+        page?: number
+      }
+    | undefined
+  >()
 
   routerAlwaysTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+    .toEqualTypeOf<
+      | {
+          rootPage?: number
+          page?: number
+        }
+      | undefined
+    >()
 
   routerNeverTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+    .toEqualTypeOf<
+      | {
+          rootPage?: number
+          page?: number
+        }
+      | undefined
+    >()
 
   routerPreserveTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+    .toEqualTypeOf<
+      | {
+          rootPage?: number
+          page?: number
+        }
+      | undefined
+    >()
 
-  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page?: number }>()
+  defaultRouterLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
+    page?: number
+  }>()
 
-  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<{ page?: number }>()
+  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
+    page?: number
+  }>()
 
   routerAlwaysTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
     page?: number
   }>()
 
   routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
     page?: number
   }>()
 
   routerPreserveTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
     page?: number
   }>()
 
-  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{ page?: number }>()
+  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{ page?: number }>()
+  defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerAlwaysTrailingSlashesLinkSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerAlwaysTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerNeverTrailingSlashesLinkSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerNeverTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerPreserveTrailingSlashesLinkSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerPreserveTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 })
 
 test('when navigating from a route with no search params to a route with search params', () => {
@@ -2298,47 +2518,72 @@ test('when navigating from a route with no search params to a route with search
     .parameter(0)
     .toMatchTypeOf<{ search: unknown }>()
 
-  defaultRouterLinkSearch
-    .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+  defaultRouterLinkSearch.exclude<Function | boolean>().toEqualTypeOf<{
+    rootPage?: number
+    page: number
+  }>()
 
-  defaultRouterObjectsLinkSearch
-    .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+  defaultRouterObjectsLinkSearch.exclude<Function | boolean>().toEqualTypeOf<{
+    rootPage?: number
+    page: number
+  }>()
 
   routerAlwaysTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+    .toEqualTypeOf<{
+      rootPage?: number
+      page: number
+    }>()
 
   routerNeverTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number }>()
+    .toEqualTypeOf<{
+      rootPage?: number
+      page: number
+    }>()
 
-  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page: number }>()
+  defaultRouterLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
+    page: number
+  }>()
 
-  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<{ page: number }>()
+  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
+    page: number
+  }>()
 
   routerAlwaysTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
     page: number
   }>()
 
   routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
     page: number
   }>()
 
   routerPreserveTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    rootPage?: number
     page: number
   }>()
 
-  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{}>()
+  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{ rootPage?: number }>()
 
-  defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{}>()
+  defaultRouterObjectsLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
 
-  routerAlwaysTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{}>()
+  routerAlwaysTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
 
-  routerNeverTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{}>()
+  routerNeverTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
 
-  routerPreserveTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{}>()
+  routerPreserveTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{ rootPage?: number }>()
 })
 
 test('when navigating to a union of routes with search params', () => {
@@ -2423,71 +2668,83 @@ test('when navigating to a union of routes with search params', () => {
 
   defaultRouterLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      { rootPage?: number; page: number } | { rootPage?: number } | undefined
+    >()
 
   defaultRouterObjectsLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      { rootPage?: number; page: number } | { rootPage?: number } | undefined
+    >()
 
   routerAlwaysTrailingSlashesSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      { rootPage?: number; page: number } | { rootPage?: number } | undefined
+    >()
 
   routerNeverTrailingSlashesSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      { rootPage?: number; page: number } | { rootPage?: number } | undefined
+    >()
 
   routerPreserveTrailingSlashesSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
-
-  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page: number } | {}>()
-
-  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<{ page: number } | {}>()
-
-  routerAlwaysTrailingSlashesSearch.returns.toEqualTypeOf<
-    { page: number } | {}
-  >()
+    .toEqualTypeOf<
+      { rootPage?: number; page: number } | { rootPage?: number } | undefined
+    >()
 
-  routerNeverTrailingSlashesSearch.returns.toEqualTypeOf<
-    { page: number } | {}
+  defaultRouterLinkSearch.returns.toEqualTypeOf<
+    { rootPage?: number; page: number } | { rootPage?: number }
   >()
 
-  routerPreserveTrailingSlashesSearch.returns.toEqualTypeOf<
-    { page: number } | {}
+  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<
+    { rootPage?: number; page: number } | { rootPage?: number }
   >()
 
-  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page: number } | {}>()
-
-  defaultRouterObjectsLinkSearch.returns.toEqualTypeOf<{ page: number } | {}>()
-
   routerAlwaysTrailingSlashesSearch.returns.toEqualTypeOf<
-    { page: number } | {}
+    { rootPage?: number; page: number } | { rootPage?: number }
   >()
 
   routerNeverTrailingSlashesSearch.returns.toEqualTypeOf<
-    { page: number } | {}
+    { rootPage?: number; page: number } | { rootPage?: number }
   >()
 
   routerPreserveTrailingSlashesSearch.returns.toEqualTypeOf<
-    { page: number } | {}
+    { rootPage?: number; page: number } | { rootPage?: number }
   >()
 
-  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{ page?: number }>()
+  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{ page?: number }>()
+  defaultRouterObjectsLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerAlwaysTrailingSlashesSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerAlwaysTrailingSlashesSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerNeverTrailingSlashesSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerNeverTrailingSlashesSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerPreserveTrailingSlashesSearch
-    .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+  routerPreserveTrailingSlashesSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 })
 
 test('when navigating to a union of routes with search params including the root', () => {
@@ -2498,7 +2755,7 @@ test('when navigating to a union of routes with search params including the root
   >
 
   const DefaultRouterObjectsLink = Link<
-    DefaultRouter,
+    DefaultRouterObjects,
     string,
     '/' | '/invoices/$invoiceId/edit' | '/posts/$postId'
   >
@@ -2573,53 +2830,243 @@ test('when navigating to a union of routes with search params including the root
 
   defaultRouterSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      | { rootPage?: number }
+      | { rootPage?: number; page: number }
+      | { rootPage?: number; rootIndexPage: number }
+      | undefined
+    >()
 
   defaultRouterObjectsSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      | { rootPage?: number }
+      | { rootPage?: number; page: number }
+      | { rootPage?: number; rootIndexPage: number }
+      | undefined
+    >()
 
   routerAlwaysTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      | { rootPage?: number }
+      | { rootPage?: number; page: number }
+      | { rootPage?: number; rootIndexPage: number }
+      | undefined
+    >()
 
   routerNeverTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      | { rootPage?: number }
+      | { rootPage?: number; page: number }
+      | { rootPage?: number; rootIndexPage: number }
+      | undefined
+    >()
 
   routerPreserveTrailingSlashesLinkSearch
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+    .toEqualTypeOf<
+      | { rootPage?: number }
+      | { rootPage?: number; page: number }
+      | { rootPage?: number; rootIndexPage: number }
+      | undefined
+    >()
 
-  defaultRouterSearch.returns.toEqualTypeOf<{ page: number } | {}>()
+  defaultRouterSearch.returns.toEqualTypeOf<
+    | { rootPage?: number; page: number }
+    | { rootPage?: number; rootIndexPage: number }
+    | { rootPage?: number }
+  >()
 
-  defaultRouterObjectsSearch.returns.toEqualTypeOf<{ page: number } | {}>()
+  defaultRouterObjectsSearch.returns.toEqualTypeOf<
+    | { rootPage?: number; page: number }
+    | { rootPage?: number; rootIndexPage: number }
+    | { rootPage?: number }
+  >()
 
   routerAlwaysTrailingSlashesLinkSearch.returns.toEqualTypeOf<
-    { page: number } | {}
+    | { rootPage?: number; page: number }
+    | { rootPage?: number; rootIndexPage: number }
+    | { rootPage?: number }
   >()
 
   routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<
-    { page: number } | {}
+    | { rootPage?: number; page: number }
+    | { rootPage?: number; rootIndexPage: number }
+    | { rootPage?: number }
   >()
 
   routerPreserveTrailingSlashesLinkSearch.returns.toEqualTypeOf<
-    { page: number } | {}
+    | { rootPage?: number; page: number }
+    | { rootPage?: number; rootIndexPage: number }
+    | { rootPage?: number }
   >()
 
-  defaultRouterSearch.parameter(0).toEqualTypeOf<{ page?: number }>()
+  defaultRouterSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
+
+  defaultRouterObjectsSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  defaultRouterObjectsSearch.parameter(0).toEqualTypeOf<{ page?: number }>()
+  routerAlwaysTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
 
-  routerAlwaysTrailingSlashesLinkSearch
+  routerNeverTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
+
+  routerPreserveTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{
+    page?: number
+    rootIndexPage?: number
+    rootPage?: number
+  }>()
+})
+
+test('when navigating from the root to /posts', () => {
+  const DefaultRouterLink = Link<DefaultRouter, '/', '/posts'>
+
+  const DefaultRouterObjectsLink = Link<DefaultRouterObjects, '/', '/posts'>
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    '/',
+    '/posts/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/',
+    '/posts'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    '/',
+    '/posts' | '/posts/'
+  >
+
+  expectTypeOf(DefaultRouterLink).not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(DefaultRouterObjectsLink).not.toMatchTypeOf<{
+    search: unknown
+  }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink).not.toMatchTypeOf<{
+    search: unknown
+  }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink).not.toMatchTypeOf<{
+    search: unknown
+  }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink).not.toMatchTypeOf<{
+    search: unknown
+  }>()
+
+  expectTypeOf(DefaultRouterLink)
     .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number } | undefined>()
 
-  routerNeverTrailingSlashesLinkSearch
+  expectTypeOf(DefaultRouterObjectsLink)
     .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number } | undefined>()
 
-  routerPreserveTrailingSlashesLinkSearch
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number } | undefined>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number } | undefined>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ rootPage?: number } | undefined>()
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<
+      { rootPage?: number } | { rootPage?: number; rootIndexPage: number }
+    >()
+
+  expectTypeOf(DefaultRouterObjectsLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<
+      { rootPage?: number } | { rootPage?: number; rootIndexPage: number }
+    >()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<
+      { rootPage?: number } | { rootPage?: number; rootIndexPage: number }
+    >()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<
+      { rootPage?: number } | { rootPage?: number; rootIndexPage: number }
+    >()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .parameter(0)
+    .toEqualTypeOf<
+      { rootPage?: number } | { rootPage?: number; rootIndexPage: number }
+    >()
+
+  expectTypeOf(DefaultRouterLink)
     .parameter(0)
-    .toEqualTypeOf<{ page?: number }>()
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(DefaultRouterObjectsLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .returns.toEqualTypeOf<{ rootPage?: number }>()
 })
diff --git a/packages/react-router/tests/useSearch.test-d.tsx b/packages/react-router/tests/useSearch.test-d.tsx
index 9687bfb5d..73af7e8b5 100644
--- a/packages/react-router/tests/useSearch.test-d.tsx
+++ b/packages/react-router/tests/useSearch.test-d.tsx
@@ -279,3 +279,206 @@ test('when there are overlapping search params', () => {
       | undefined
     >()
 })
+
+test('when the root has no search params but the index route does', () => {
+  const rootRoute = createRootRoute()
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: () => ({ indexPage: 0 }),
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{ indexPage: number }>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '__root__'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{}>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<((search: {}) => {}) | undefined>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ indexPage?: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { indexPage?: number }) => { indexPage?: number }) | undefined
+    >()
+})
+
+test('when the root has search params but the index route does not', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: () => ({ rootPage: 0 }),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{ rootPage: number }>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '__root__'>,
+  ).returns.toEqualTypeOf<{ rootPage: number }>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{ rootPage: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { rootPage: number }) => { rootPage: number }) | undefined
+    >()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ rootPage?: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { rootPage?: number }) => { rootPage?: number }) | undefined
+    >()
+})
+
+test('when the root has search params but the index does', () => {
+  const rootRoute = createRootRoute({
+    validateSearch: () => ({ rootPage: 0 }),
+  })
+
+  const indexRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: '/',
+    validateSearch: () => ({ indexPage: 0 }),
+  })
+
+  const invoicesRoute = createRoute({
+    getParentRoute: () => rootRoute,
+    path: 'invoices',
+  })
+
+  const invoicesIndexRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '/',
+  })
+
+  const invoiceRoute = createRoute({
+    getParentRoute: () => invoicesRoute,
+    path: '$invoiceId',
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+    indexRoute,
+  ])
+
+  const defaultRouter = createRouter({
+    routeTree,
+  })
+
+  type DefaultRouter = typeof defaultRouter
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/'>,
+  ).returns.toEqualTypeOf<{ rootPage: number; indexPage: number }>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '__root__'>,
+  ).returns.toEqualTypeOf<{ rootPage: number }>()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices'>,
+  ).returns.toEqualTypeOf<{ rootPage: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      ((search: { rootPage: number }) => { rootPage: number }) | undefined
+    >()
+
+  expectTypeOf(
+    useSearch<DefaultRouter['routeTree'], '/invoices', false>,
+  ).returns.toEqualTypeOf<{ indexPage?: number; rootPage?: number }>()
+
+  expectTypeOf(useSearch<DefaultRouter['routeTree'], '/invoices', false>)
+    .parameter(0)
+    .toHaveProperty('select')
+    .toEqualTypeOf<
+      | ((search: { indexPage?: number; rootPage?: number }) => {
+          indexPage?: number
+          rootPage?: number
+        })
+      | undefined
+    >()
+})