Repository: TanStack/router
Pull request #1509 (2024-04-22 22:03:44)

feat: `trailingSlashes` option on the type level
We can infer `trailingSlashes` from the router and use it to enforce the `to` prop when navigating.

`trailingSlashes` currently has three options: always | never | preserve.

always - we always have a trailing slash on leaves autocompleted with to. Branches are not affected in from as you might want a loose match on this for any leaves under a branch instead of an index route.

never - we never have a trailing slash on leaves when autocompleted on to. from does not change as mentioned in always

preserve - to autocompletes to include both  trailing slash or without in the union. It can be either.

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

CI is running for commit c21a33f0687ede02c29298b0a6b6ec456f685de7.

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


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)
chorobin: Don't merge yet. I want to add more type tests to cover over parts that could have regressed
chorobin: Should be ok to merge when approved 

Patch:
diff --git a/packages/react-router/src/Matches.tsx b/packages/react-router/src/Matches.tsx
index 10045c6bc..7967685a3 100644
--- a/packages/react-router/src/Matches.tsx
+++ b/packages/react-router/src/Matches.tsx
@@ -428,37 +428,39 @@ export interface MatchRouteOptions {
 }
 
 export type UseMatchRouteOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> = RoutePaths<TRouteTree>,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> = RoutePaths<
+    TRouter['routeTree']
+  >,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> = TFrom,
   TMaskTo extends string = '',
   TOptions extends ToOptions<
-    TRouteTree,
+    TRouter,
     TFrom,
     TTo,
     TMaskFrom,
     TMaskTo
-  > = ToOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
+  > = ToOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
   TRelaxedOptions = Omit<TOptions, 'search' | 'params'> &
     DeepPartial<Pick<TOptions, 'search' | 'params'>>,
 > = TRelaxedOptions & MatchRouteOptions
 
-export function useMatchRoute<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
->() {
+export function useMatchRoute<TRouter extends AnyRouter = RegisteredRouter>() {
   const router = useRouter()
 
   return React.useCallback(
     <
-      TFrom extends RoutePaths<TRouteTree> = RoutePaths<TRouteTree>,
+      TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
       TTo extends string = '',
-      TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
+      TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
       TMaskTo extends string = '',
       TResolved extends string = ResolveRelativePath<TFrom, NoInfer<TTo>>,
     >(
-      opts: UseMatchRouteOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
-    ): false | RouteById<TRouteTree, TResolved>['types']['allParams'] => {
+      opts: UseMatchRouteOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
+    ):
+      | false
+      | RouteById<TRouter['routeTree'], TResolved>['types']['allParams'] => {
       const { pending, caseSensitive, fuzzy, includeSearch, ...rest } = opts
 
       return router.matchRoute(rest as any, {
@@ -473,17 +475,25 @@ export function useMatchRoute<
 }
 
 export type MakeMatchRouteOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> = RoutePaths<TRouteTree>,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> = RoutePaths<
+    TRouter['routeTree']
+  >,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> = TFrom,
   TMaskTo extends string = '',
-> = UseMatchRouteOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
+> = UseMatchRouteOptions<
+  TRouter['routeTree'],
+  TFrom,
+  TTo,
+  TMaskFrom,
+  TMaskTo
+> & {
   // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
   children?:
     | ((
         params?: RouteByPath<
-          TRouteTree,
+          TRouter['routeTree'],
           ResolveRelativePath<TFrom, NoInfer<TTo>>
         >['types']['allParams'],
       ) => ReactNode)
@@ -491,13 +501,21 @@ export type MakeMatchRouteOptions<
 }
 
 export function MatchRoute<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> = RoutePaths<TRouteTree>,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> = RoutePaths<
+    TRouter['routeTree']
+  >,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> = TFrom,
   TMaskTo extends string = '',
 >(
-  props: MakeMatchRouteOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
+  props: MakeMatchRouteOptions<
+    TRouter['routeTree'],
+    TFrom,
+    TTo,
+    TMaskFrom,
+    TMaskTo
+  >,
 ): any {
   const matchRoute = useMatchRoute()
   const params = matchRoute(props as any)
diff --git a/packages/react-router/src/RouterProvider.tsx b/packages/react-router/src/RouterProvider.tsx
index 1bc7abc75..207c3abdd 100644
--- a/packages/react-router/src/RouterProvider.tsx
+++ b/packages/react-router/src/RouterProvider.tsx
@@ -10,6 +10,7 @@ import type { ParsedLocation } from './location'
 import type { AnyRoute } from './route'
 import type { RoutePaths } from './routeInfo'
 import type {
+  AnyRouter,
   RegisteredRouter,
   Router,
   RouterOptions,
@@ -37,12 +38,12 @@ export interface MatchLocation {
 
 export type NavigateFn = <
   TTo extends string,
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
 >(
-  opts: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
+  opts: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
 ) => Promise<void>
 
 export type BuildLocationFn<TRouteTree extends AnyRoute> = <
@@ -51,7 +52,13 @@ export type BuildLocationFn<TRouteTree extends AnyRoute> = <
   TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
   TMaskTo extends string = '',
 >(
-  opts: ToOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
+  opts: ToOptions<
+    Router<TRouteTree, 'never'>,
+    TFrom,
+    TTo,
+    TMaskFrom,
+    TMaskTo
+  > & {
     leaveParams?: boolean
   },
 ) => ParsedLocation
@@ -59,9 +66,9 @@ export type BuildLocationFn<TRouteTree extends AnyRoute> = <
 export type InjectedHtmlEntry = string | (() => Promise<string> | string)
 
 export function RouterProvider<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
+  TRouter extends AnyRouter = RegisteredRouter,
   TDehydrated extends Record<string, any> = Record<string, any>,
->({ router, ...rest }: RouterProps<TRouteTree, TDehydrated>) {
+>({ router, ...rest }: RouterProps<TRouter, TDehydrated>) {
   // Allow the router to update options on the router instance
   router.update({
     ...router.options,
@@ -238,11 +245,27 @@ export function getRouteMatch<TRouteTree extends AnyRoute>(
 }
 
 export type RouterProps<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
+  TRouter extends AnyRouter = RegisteredRouter,
   TDehydrated extends Record<string, any> = Record<string, any>,
-> = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> & {
-  router: Router<TRouteTree>
-  context?: Partial<RouterOptions<TRouteTree, TDehydrated>['context']>
+> = Omit<
+  RouterOptions<
+    TRouter['routeTree'],
+    NonNullable<TRouter['options']['trailingSlash']>,
+    TDehydrated
+  >,
+  'context'
+> & {
+  router: Router<
+    TRouter['routeTree'],
+    NonNullable<TRouter['options']['trailingSlash']>
+  >
+  context?: Partial<
+    RouterOptions<
+      TRouter['routeTree'],
+      NonNullable<TRouter['options']['trailingSlash']>,
+      TDehydrated
+    >['context']
+  >
 }
 
 function usePrevious<T>(value: T) {
diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx
index cae8bfc5e..008391f63 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -4,20 +4,20 @@ import { useMatch } from './Matches'
 import { useRouterState } from './useRouterState'
 import { useRouter } from './useRouter'
 import { deepEqual, exactPathTest, functionalUpdate } from './utils'
-import type { ParsedLocation } from '.'
+import type { AnyRouter, ParsedLocation } from '.'
 import type { HistoryState } from '@tanstack/history'
 import type { Trim } from './fileRoute'
 import type { AnyRoute, RootSearchSchema } from './route'
 import type {
   RouteByPath,
-  RouteLeaves,
+  RouteByToPath,
   RoutePaths,
   RoutePathsAutoComplete,
+  RouteToPath,
 } from './routeInfo'
 import type { RegisteredRouter } from './router'
 import type {
   Expand,
-  IsUnion,
   MakeDifferenceOptional,
   NoInfer,
   NonNullableUpdater,
@@ -79,94 +79,96 @@ export type Last<T extends Array<any>> = T extends [...infer _, infer L]
   ? L
   : never
 
-export type RemoveTrailingSlashes<T> = T extends `${infer R}/`
-  ? RemoveTrailingSlashes<R>
-  : T
+export type RemoveTrailingSlashes<T> = T extends `${infer R}/` ? R : T
 
-export type RemoveLeadingSlashes<T> = T extends `/${infer R}`
-  ? RemoveLeadingSlashes<R>
-  : T
+export type RemoveLeadingSlashes<T> = T extends `/${infer R}` ? R : T
 
-export type ResolvePaths<TRouteTree extends AnyRoute, TSearchPath> =
-  RouteByPath<TRouteTree, RemoveTrailingSlashes<TSearchPath>> extends never
-    ? RouteLeaves<TRouteTree>
-    : RouteLeaves<RouteByPath<TRouteTree, RemoveTrailingSlashes<TSearchPath>>>
+export type ResolvePaths<TRouter extends AnyRouter, TSearchPath> =
+  RouteByPath<
+    TRouter['routeTree'],
+    RemoveTrailingSlashes<TSearchPath>
+  > extends never
+    ? RouteToPath<TRouter, TRouter['routeTree']>
+    : RouteToPath<
+        TRouter,
+        RouteByPath<TRouter['routeTree'], RemoveTrailingSlashes<TSearchPath>>
+      >
 
 export type SearchPaths<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TSearchPath extends string,
-  TPaths = ResolvePaths<TRouteTree, TSearchPath>,
-> = TPaths extends `${RemoveTrailingSlashes<TSearchPath>}/${infer TRest}`
+  TPaths = ResolvePaths<TRouter, TSearchPath>,
+> = TPaths extends `${RemoveTrailingSlashes<TSearchPath>}${infer TRest}`
   ? TRest
   : never
 
 export type SearchRelativePathAutoComplete<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TTo extends string,
   TSearchPath extends string,
-> = `${TTo}/${SearchPaths<TRouteTree, TSearchPath>}`
+> = `${TTo}/${RemoveLeadingSlashes<SearchPaths<TRouter, TSearchPath>>}`
 
 export type RelativeToParentPathAutoComplete<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TFrom extends string,
   TTo extends string,
   TResolvedPath extends string = RemoveTrailingSlashes<
     ResolveRelativePath<TFrom, TTo>
   >,
 > =
-  | SearchRelativePathAutoComplete<TRouteTree, TTo, TResolvedPath>
+  | SearchRelativePathAutoComplete<TRouter, TTo, TResolvedPath>
   | (TResolvedPath extends '' ? never : `${TTo}/../`)
 
 export type RelativeToCurrentPathAutoComplete<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TFrom extends string,
   TTo extends string,
   TRestTo extends string,
   TResolvedPath extends
     string = RemoveTrailingSlashes<`${RemoveTrailingSlashes<TFrom>}/${RemoveLeadingSlashes<TRestTo>}`>,
-> = SearchRelativePathAutoComplete<TRouteTree, TTo, TResolvedPath>
+> = SearchRelativePathAutoComplete<TRouter, TTo, TResolvedPath>
 
 export type AbsolutePathAutoComplete<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TFrom extends string,
 > =
   | (string extends TFrom
       ? './'
       : TFrom extends `/`
         ? never
-        : SearchPaths<TRouteTree, TFrom> extends ''
+        : SearchPaths<TRouter, TFrom> extends ''
           ? never
           : './')
   | (string extends TFrom ? '../' : TFrom extends `/` ? never : '../')
-  | RouteLeaves<TRouteTree>
-  | (TFrom extends '/' ? never : SearchPaths<TRouteTree, TFrom>)
+  | RouteToPath<TRouter, TRouter['routeTree']>
+  | (TFrom extends '/'
+      ? never
+      : string extends TFrom
+        ? RemoveLeadingSlashes<RouteToPath<TRouter, TRouter['routeTree']>>
+        : RemoveLeadingSlashes<SearchPaths<TRouter, TFrom>>)
 
 export type RelativeToPathAutoComplete<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TFrom extends string,
   TTo extends string,
 > = TTo extends `..${string}`
-  ? RelativeToParentPathAutoComplete<
-      TRouteTree,
-      TFrom,
-      RemoveTrailingSlashes<TTo>
-    >
+  ? RelativeToParentPathAutoComplete<TRouter, TFrom, RemoveTrailingSlashes<TTo>>
   : TTo extends `./${infer TRestTTo}`
     ? RelativeToCurrentPathAutoComplete<
-        TRouteTree,
+        TRouter,
         TFrom,
         RemoveTrailingSlashes<TTo>,
         TRestTTo
       >
-    : AbsolutePathAutoComplete<TRouteTree, TFrom>
+    : AbsolutePathAutoComplete<TRouter, TFrom>
 
 export type NavigateOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
-> = ToOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
+> = ToOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
   // `replace` is a boolean that determines whether the navigation should replace the current history entry or push a new one.
   replace?: boolean
   resetScroll?: boolean
@@ -177,37 +179,37 @@ export type NavigateOptions<
 }
 
 export type ToOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
-> = ToSubOptions<TRouteTree, TFrom, TTo> & {
+> = ToSubOptions<TRouter, TFrom, TTo> & {
   _fromLocation?: ParsedLocation
-  mask?: ToMaskOptions<TRouteTree, TMaskFrom, TMaskTo>
+  mask?: ToMaskOptions<TRouter, TMaskFrom, TMaskTo>
 }
 
 export type ToMaskOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TMaskFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouteTree extends AnyRouter = RegisteredRouter,
+  TMaskFrom extends RoutePaths<TRouteTree['routeTree']> | string = string,
   TMaskTo extends string = '',
 > = ToSubOptions<TRouteTree, TMaskFrom, TMaskTo> & {
   unmaskOnReload?: boolean
 }
 
 export type ToSubOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
 > = {
-  to?: ToPathOption<TRouteTree, TFrom, TTo> & {}
+  to?: ToPathOption<TRouter, TFrom, TTo> & {}
   hash?: true | Updater<string>
   state?: true | NonNullableUpdater<HistoryState>
   // The source route path. This is automatically set when using route-level APIs, but for type-safe relative routing on the router itself, this is required
-  from?: RoutePathsAutoComplete<TRouteTree, TFrom> & {}
+  from?: RoutePathsAutoComplete<TRouter['routeTree'], TFrom> & {}
   // // When using relative route paths, this option forces resolution from the current path, instead of the route API's path or `from` path
-} & SearchParamOptions<TRouteTree, TFrom, TTo> &
-  PathParamOptions<TRouteTree, TFrom, TTo>
+} & SearchParamOptions<TRouter, TFrom, TTo> &
+  PathParamOptions<TRouter, TFrom, TTo>
 
 type ParamsReducer<TFrom, TTo> = TTo | ((current: TFrom) => TTo)
 
@@ -220,20 +222,18 @@ type ExcludeRootSearchSchema<T, TExcluded = Exclude<T, RootSearchSchema>> = [
   : TExcluded
 
 export type ResolveRoute<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TFrom,
   TTo,
-  TPath = RemoveTrailingSlashes<
-    string extends TFrom
-      ? TTo
-      : string extends TTo
-        ? TFrom
-        : ResolveRelativePath<TFrom, TTo>
-  >,
+  TPath = string extends TFrom
+    ? TTo
+    : string extends TTo
+      ? TFrom
+      : ResolveRelativePath<TFrom, TTo>,
 > = TPath extends string
-  ? RouteByPath<TRouteTree, `${TPath}/`> extends never
-    ? RouteByPath<TRouteTree, TPath>
-    : RouteByPath<TRouteTree, `${TPath}/`>
+  ? string extends TTo
+    ? RouteByPath<TRouter['routeTree'], TPath>
+    : RouteByToPath<TRouter, TPath>
   : never
 
 type PostProcessParams<
@@ -242,25 +242,22 @@ type PostProcessParams<
 > = TParamVariant extends 'SEARCH' ? ExcludeRootSearchSchema<T> : T
 
 type ResolveFromParams<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TParamVariant extends ParamVariant,
   TFrom,
-  TFromRouteType extends
-    | 'allParams'
-    | 'fullSearchSchema' = TParamVariant extends 'PATH'
-    ? 'allParams'
-    : 'fullSearchSchema',
 > = PostProcessParams<
-  RouteByPath<TRouteTree, TFrom>['types'][TFromRouteType],
+  RouteByPath<TRouter['routeTree'], TFrom>['types'][TParamVariant extends 'PATH'
+    ? 'allParams'
+    : 'fullSearchSchema'],
   TParamVariant
 >
 
 type ResolveToParams<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TParamVariant extends ParamVariant,
   TFrom,
   TTo,
-  TRoute extends AnyRoute = ResolveRoute<TRouteTree, TFrom, TTo>,
+  TRoute extends AnyRoute = ResolveRoute<TRouter, TFrom, TTo>,
 > = PostProcessParams<
   TRoute['types'][TParamVariant extends 'PATH'
     ? 'allParams'
@@ -269,22 +266,22 @@ type ResolveToParams<
 >
 
 type ResolveRelativeToParams<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TParamVariant extends ParamVariant,
   TFrom,
   TTo,
-  TToParams = ResolveToParams<TRouteTree, TParamVariant, TFrom, TTo>,
+  TToParams = ResolveToParams<TRouter, TParamVariant, TFrom, TTo>,
 > = TParamVariant extends 'SEARCH'
   ? TToParams
   : string extends TFrom
     ? TToParams
     : MakeDifferenceOptional<
-        ResolveFromParams<TRouteTree, TParamVariant, TFrom>,
+        ResolveFromParams<TRouter, TParamVariant, TFrom>,
         TToParams
       >
 
 type MakeOptionalParams<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TParamVariant extends ParamVariant,
   TFrom,
   TTo,
@@ -293,29 +290,25 @@ type MakeOptionalParams<
       search?:
         | true
         | (ParamsReducer<
-            Expand<ResolveFromParams<TRouteTree, TParamVariant, TFrom>>,
-            Expand<
-              ResolveRelativeToParams<TRouteTree, TParamVariant, TFrom, TTo>
-            >
+            Expand<ResolveFromParams<TRouter, TParamVariant, TFrom>>,
+            Expand<ResolveRelativeToParams<TRouter, TParamVariant, TFrom, TTo>>
           > & {})
     }
   : {
       params?:
         | true
         | (ParamsReducer<
-            Expand<ResolveFromParams<TRouteTree, TParamVariant, TFrom>>,
-            Expand<
-              ResolveRelativeToParams<TRouteTree, TParamVariant, TFrom, TTo>
-            >
+            Expand<ResolveFromParams<TRouter, TParamVariant, TFrom>>,
+            Expand<ResolveRelativeToParams<TRouter, TParamVariant, TFrom, TTo>>
           > & {})
     }
 
 type MakeRequiredParamsReducer<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TParamVariant extends ParamVariant,
   TFrom,
   TToParams,
-  TFromParams = ResolveFromParams<TRouteTree, TParamVariant, TFrom>,
+  TFromParams = ResolveFromParams<TRouter, TParamVariant, TFrom>,
 > =
   | ([TFromParams] extends [WithoutEmpty<PickRequired<TToParams>>]
       ? true
@@ -323,7 +316,7 @@ type MakeRequiredParamsReducer<
   | ParamsReducer<Expand<TFromParams>, TToParams>
 
 export type MakeRequiredParams<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TParamVariant extends ParamVariant,
   TFrom,
   TTo,
@@ -331,20 +324,20 @@ export type MakeRequiredParams<
   ? {
       search: Expand<
         MakeRequiredParamsReducer<
-          TRouteTree,
+          TRouter,
           TParamVariant,
           TFrom,
-          Expand<ResolveRelativeToParams<TRouteTree, TParamVariant, TFrom, TTo>>
+          Expand<ResolveRelativeToParams<TRouter, TParamVariant, TFrom, TTo>>
         >
       > & {}
     }
   : {
       params: Expand<
         MakeRequiredParamsReducer<
-          TRouteTree,
+          TRouter,
           TParamVariant,
           TFrom,
-          Expand<ResolveRelativeToParams<TRouteTree, TParamVariant, TFrom, TTo>>
+          Expand<ResolveRelativeToParams<TRouter, TParamVariant, TFrom, TTo>>
         >
       > & {}
     }
@@ -359,7 +352,7 @@ export type IsRequiredParams<TParams> = keyof TParams extends infer K extends
   : never
 
 export type IsRequired<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TParamVariant extends ParamVariant,
   TFrom,
   TTo,
@@ -367,42 +360,42 @@ export type IsRequired<
   ? string extends TFrom
     ? never
     : IsRequiredParams<
-        ResolveRelativeToParams<TRouteTree, TParamVariant, TFrom, TTo>
+        ResolveRelativeToParams<TRouter, TParamVariant, TFrom, TTo>
       >
   : IsRequiredParams<
-      ResolveRelativeToParams<TRouteTree, TParamVariant, TFrom, TTo>
+      ResolveRelativeToParams<TRouter, TParamVariant, TFrom, TTo>
     >
 
 export type ParamOptions<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TFrom,
   TTo extends string,
   TParamVariant extends ParamVariant,
 > =
-  IsRequired<TRouteTree, TParamVariant, TFrom, TTo> extends never
-    ? MakeOptionalParams<TRouteTree, TParamVariant, TFrom, TTo>
-    : MakeRequiredParams<TRouteTree, TParamVariant, TFrom, TTo>
+  IsRequired<TRouter, TParamVariant, TFrom, TTo> extends never
+    ? MakeOptionalParams<TRouter, TParamVariant, TFrom, TTo>
+    : MakeRequiredParams<TRouter, TParamVariant, TFrom, TTo>
 
 export type SearchParamOptions<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TFrom,
   TTo extends string,
-> = ParamOptions<TRouteTree, TFrom, TTo, 'SEARCH'>
+> = ParamOptions<TRouter, TFrom, TTo, 'SEARCH'>
 
 export type PathParamOptions<
-  TRouteTree extends AnyRoute,
+  TRouter extends AnyRouter,
   TFrom,
   TTo extends string,
-> = ParamOptions<TRouteTree, TFrom, TTo, 'PATH'>
+> = ParamOptions<TRouter, TFrom, TTo, 'PATH'>
 
 export type ToPathOption<
-  TRouteTree extends AnyRoute = AnyRoute,
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = AnyRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = string,
 > =
-  | CheckPath<TRouteTree, TTo, never, TFrom, TTo>
+  | CheckPath<TRouter, TTo, never, TFrom, TTo>
   | RelativeToPathAutoComplete<
-      TRouteTree,
+      TRouter,
       NoInfer<TFrom> extends string ? NoInfer<TFrom> : '',
       NoInfer<TTo> & string
     >
@@ -414,12 +407,12 @@ export interface ActiveOptions {
 }
 
 export type LinkOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
-> = NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
+> = NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
   // The standard anchor tag target attribute
   target?: HTMLAnchorElement['target']
   // Defaults to `{ exact: false, includeHash: false }`
@@ -432,16 +425,8 @@ export type LinkOptions<
   disabled?: boolean
 }
 
-export type CheckPath<TRouteTree extends AnyRoute, TPass, TFail, TFrom, TTo> =
-  ResolveRoute<TRouteTree, TFrom, TTo> extends infer TRoute extends AnyRoute
-    ? [TRoute] extends [never]
-      ? TFail
-      : string extends TTo
-        ? TPass
-        : unknown extends TRoute['children']
-          ? TPass
-          : TFail
-    : TFail
+export type CheckPath<TRouter extends AnyRouter, TPass, TFail, TFrom, TTo> =
+  ResolveRoute<TRouter, TFrom, TTo> extends never ? TFail : TPass
 
 export type ResolveRelativePath<TFrom, TTo = '.'> = TFrom extends string
   ? TTo extends string
@@ -493,13 +478,13 @@ type LinkCurrentTargetElement = {
 const preloadWarning = 'Error preloading route! ☝️'
 
 export function useLinkProps<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
 >(
-  options: UseLinkPropsOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
+  options: UseLinkPropsOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
 ): React.AnchorHTMLAttributes<HTMLAnchorElement> {
   const router = useRouter()
   const matchPathname = useMatch({
@@ -739,21 +724,21 @@ export function useLinkProps<
 }
 
 export type UseLinkPropsOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
-> = ActiveLinkOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
+> = ActiveLinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
   React.AnchorHTMLAttributes<HTMLAnchorElement>
 
 export type ActiveLinkOptions<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
-> = LinkOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
+> = LinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
   // A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
   activeProps?:
     | React.AnchorHTMLAttributes<HTMLAnchorElement>
@@ -765,12 +750,12 @@ export type ActiveLinkOptions<
 }
 
 export type LinkProps<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = string,
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
-> = ActiveLinkOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> & {
+> = ActiveLinkOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
   // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
   children?:
     | React.ReactNode
@@ -798,13 +783,13 @@ type LinkComponentProps<TComp> = React.PropsWithoutRef<
   >
 
 export type LinkComponent<TComp> = <
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
 >(
-  props: LinkProps<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> &
+  props: LinkProps<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> &
     LinkComponentProps<TComp>,
 ) => React.ReactElement
 
diff --git a/packages/react-router/src/redirects.ts b/packages/react-router/src/redirects.ts
index 41f99c856..7c24098c4 100644
--- a/packages/react-router/src/redirects.ts
+++ b/packages/react-router/src/redirects.ts
@@ -1,16 +1,16 @@
 import type { NavigateOptions } from './link'
 import type { AnyRoute } from './route'
 import type { RoutePaths } from './routeInfo'
-import type { RegisteredRouter } from './router'
+import type { AnyRouter, RegisteredRouter } from './router'
 import type { PickAsRequired } from './utils'
 
 export type AnyRedirect = Redirect<any, any, any, any, any>
 
 export type Redirect<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> = '/',
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> = '/',
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> = TFrom,
   TMaskTo extends string = '',
 > = {
   /**
@@ -21,30 +21,30 @@ export type Redirect<
   statusCode?: number
   throw?: any
   headers?: HeadersInit
-} & NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>
+} & NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>
 
 export type ResolvedRedirect<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> = '/',
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> = '/',
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> = TFrom,
   TMaskTo extends string = '',
 > = PickAsRequired<
-  Redirect<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
+  Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
   'code' | 'statusCode' | 'headers'
 > & {
   href: string
 }
 
 export function redirect<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> = '/',
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
 >(
-  opts: Redirect<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
-): Redirect<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo> {
+  opts: Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
+): Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> {
   ;(opts as any).isRedirect = true
   opts.statusCode = opts.statusCode || opts.code || 301
   opts.headers = opts.headers || {}
diff --git a/packages/react-router/src/route.ts b/packages/react-router/src/route.ts
index 8229dda34..6db6a8bd0 100644
--- a/packages/react-router/src/route.ts
+++ b/packages/react-router/src/route.ts
@@ -11,7 +11,7 @@ import type { MakeRouteMatch, RouteMatch } from './Matches'
 import type { NavigateOptions, ParsePathParams, ToSubOptions } from './link'
 import type { ParsedLocation } from './location'
 import type { RouteById, RouteIds, RoutePaths } from './routeInfo'
-import type { AnyRouter, RegisteredRouter } from './router'
+import type { AnyRouter, RegisteredRouter, Router } from './router'
 import type {
   Assign,
   Expand,
@@ -360,7 +360,7 @@ export interface LoaderFnContext<
   /**
    * @deprecated Use `throw redirect({ to: '/somewhere' })` instead
    **/
-  navigate: (opts: NavigateOptions<AnyRoute>) => Promise<void>
+  navigate: (opts: NavigateOptions<AnyRouter>) => Promise<void>
   parentMatchPromise?: Promise<void>
   cause: 'preload' | 'enter' | 'stay'
   route: Route
@@ -1230,7 +1230,7 @@ export function createRouteMask<
 >(
   opts: {
     routeTree: TRouteTree
-  } & ToSubOptions<TRouteTree, TFrom, TTo>,
+  } & ToSubOptions<Router<TRouteTree, 'never'>, TFrom, TTo>,
 ): RouteMask<TRouteTree> {
   return opts as any
 }
diff --git a/packages/react-router/src/routeInfo.ts b/packages/react-router/src/routeInfo.ts
index 3e9d79664..91d8edf66 100644
--- a/packages/react-router/src/routeInfo.ts
+++ b/packages/react-router/src/routeInfo.ts
@@ -1,4 +1,5 @@
 import type { AnyRoute } from './route'
+import type { AnyRouter, Router, TrailingSlashOption } from './router'
 import type { UnionToIntersection, UnionToTuple } from './utils'
 
 export type ParseRoute<TRouteTree, TAcc = TRouteTree> = TRouteTree extends {
@@ -9,12 +10,14 @@ export type ParseRoute<TRouteTree, TAcc = TRouteTree> = TRouteTree extends {
     : TAcc
   : TAcc
 
-export type RouteLeaves<TRouteTree> =
+export type ParseRouteWithoutBranches<TRouteTree> =
   ParseRoute<TRouteTree> extends infer TRoute extends AnyRoute
     ? TRoute extends any
       ? TRoute['types']['children'] extends ReadonlyArray<any>
-        ? never
-        : TRoute['fullPath']
+        ? '/' extends TRoute['types']['children'][number]['path']
+          ? never
+          : TRoute
+        : TRoute
       : never
     : never
 
@@ -29,9 +32,14 @@ export type RouteById<TRouteTree extends AnyRoute, TId> = Extract<
 
 export type RouteIds<TRouteTree extends AnyRoute> = ParseRoute<TRouteTree>['id']
 
+export type CatchAllPaths<TRouteTree extends AnyRoute> = Record<
+  '.' | '..' | '',
+  ParseRoute<TRouteTree>
+>
+
 export type RoutesByPath<TRouteTree extends AnyRoute> = {
   [K in ParseRoute<TRouteTree> as K['fullPath']]: K
-} & Record<'.' | '..', ParseRoute<TRouteTree>>
+} & CatchAllPaths<TRouteTree>
 
 export type RouteByPath<TRouteTree extends AnyRoute, TPath> = Extract<
   string extends TPath
@@ -44,6 +52,66 @@ export type RoutePaths<TRouteTree extends AnyRoute> =
   | ParseRoute<TRouteTree>['fullPath']
   | '/'
 
+export type RouteToPathAlwaysTrailingSlash<TRoute extends AnyRoute> =
+  TRoute['path'] extends '/'
+    ? TRoute['fullPath']
+    : TRoute['fullPath'] extends '/'
+      ? TRoute['fullPath']
+      : `${TRoute['fullPath']}/`
+
+export type RouteToPathNeverTrailingSlash<TRoute extends AnyRoute> =
+  TRoute['path'] extends '/'
+    ? TRoute['fullPath'] extends '/'
+      ? TRoute['fullPath']
+      : TRoute['fullPath'] extends `${infer TRest}/`
+        ? TRest
+        : TRoute['fullPath']
+    : TRoute['fullPath']
+
+export type RouteToPathPreserveTrailingSlash<TRoute extends AnyRoute> =
+  | RouteToPathNeverTrailingSlash<TRoute>
+  | RouteToPathAlwaysTrailingSlash<TRoute>
+
+export type RouteToPathByTrailingSlashOption<TRoute extends AnyRoute> = {
+  always: RouteToPathAlwaysTrailingSlash<TRoute>
+  preserve: RouteToPathPreserveTrailingSlash<TRoute>
+  never: RouteToPathNeverTrailingSlash<TRoute>
+}
+
+export type TrailingSlashOptionByRouter<TRouter extends AnyRouter> =
+  TrailingSlashOption extends TRouter['options']['trailingSlash']
+    ? 'never'
+    : NonNullable<TRouter['options']['trailingSlash']>
+
+export type RouteToByRouter<
+  TRouter extends AnyRouter,
+  TRoute extends AnyRoute,
+> = RouteToPathByTrailingSlashOption<TRoute>[TrailingSlashOptionByRouter<TRouter>]
+
+export type RouteToPath<
+  TRouter extends AnyRouter,
+  TRouteTree extends AnyRoute,
+> =
+  ParseRouteWithoutBranches<TRouteTree> extends infer TRoute extends AnyRoute
+    ? TRoute extends any
+      ? RouteToByRouter<TRouter, TRoute>
+      : never
+    : never
+
+export type RoutesByToPath<TRouter extends AnyRouter> = {
+  [TRoute in ParseRouteWithoutBranches<TRouter['routeTree']> as RouteToByRouter<
+    TRouter,
+    TRoute
+  >]: TRoute
+} & CatchAllPaths<TRouter['routeTree']>
+
+export type RouteByToPath<TRouter extends AnyRouter, TTo> = Extract<
+  string extends TTo
+    ? ParseRouteWithoutBranches<TRouter['routeTree']>
+    : RoutesByToPath<TRouter>[TTo],
+  AnyRoute
+>
+
 export type RoutePathsAutoComplete<TRouteTree extends AnyRoute, T> =
   | (string extends T ? T & {} : T)
   | RoutePaths<TRouteTree>
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index fe4197a62..191ce8299 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -87,7 +87,7 @@ import type { DeferredPromiseState } from './defer'
 declare global {
   interface Window {
     __TSR_DEHYDRATED__?: { data: string }
-    __TSR_ROUTER_CONTEXT__?: React.Context<Router<any>>
+    __TSR_ROUTER_CONTEXT__?: React.Context<Router<any, any>>
   }
 }
 
@@ -95,7 +95,7 @@ export interface Register {
   // router: Router
 }
 
-export type AnyRouter = Router<AnyRoute, any, any>
+export type AnyRouter = Router<any, any, any, any>
 
 export type RegisteredRouter = Register extends {
   router: infer TRouter extends AnyRouter
@@ -117,8 +117,11 @@ export type RouterContextOptions<TRouteTree extends AnyRoute> =
         context: TRouteTree['types']['routerContext']
       }
 
+export type TrailingSlashOption = 'always' | 'never' | 'preserve'
+
 export interface RouterOptions<
   TRouteTree extends AnyRoute,
+  TTrailingSlashOption extends TrailingSlashOption,
   TDehydrated extends Record<string, any> = Record<string, any>,
   TSerializedError extends Record<string, any> = Record<string, any>,
 > {
@@ -157,7 +160,7 @@ export interface RouterOptions<
   defaultNotFoundComponent?: NotFoundRouteComponent
   transformer?: RouterTransformer
   errorSerializer?: RouterErrorSerializer<TSerializedError>
-  trailingSlash?: 'always' | 'never' | 'preserve'
+  trailingSlash?: TTrailingSlashOption
 }
 
 export interface RouterTransformer {
@@ -220,9 +223,18 @@ export interface DehydratedRouter {
 
 export type RouterConstructorOptions<
   TRouteTree extends AnyRoute,
+  TTrailingSlashOption extends TrailingSlashOption,
   TDehydrated extends Record<string, any>,
   TSerializedError extends Record<string, any>,
-> = Omit<RouterOptions<TRouteTree, TDehydrated, TSerializedError>, 'context'> &
+> = Omit<
+  RouterOptions<
+    TRouteTree,
+    TTrailingSlashOption,
+    TDehydrated,
+    TSerializedError
+  >,
+  'context'
+> &
   RouterContextOptions<TRouteTree>
 
 export const componentTypes = [
@@ -261,17 +273,29 @@ export type RouterListener<TRouterEvent extends RouterEvent> = {
 }
 
 export function createRouter<
-  TRouteTree extends AnyRoute = AnyRoute,
+  TRouteTree extends AnyRoute,
+  TTrailingSlashOption extends TrailingSlashOption,
   TDehydrated extends Record<string, any> = Record<string, any>,
   TSerializedError extends Record<string, any> = Record<string, any>,
 >(
-  options: RouterConstructorOptions<TRouteTree, TDehydrated, TSerializedError>,
+  options: RouterConstructorOptions<
+    TRouteTree,
+    TTrailingSlashOption,
+    TDehydrated,
+    TSerializedError
+  >,
 ) {
-  return new Router<TRouteTree, TDehydrated, TSerializedError>(options)
+  return new Router<
+    TRouteTree,
+    TTrailingSlashOption,
+    TDehydrated,
+    TSerializedError
+  >(options)
 }
 
 export class Router<
-  in out TRouteTree extends AnyRoute = AnyRoute,
+  in out TRouteTree extends AnyRoute,
+  in out TTrailingSlashOption extends TrailingSlashOption,
   in out TDehydrated extends Record<string, any> = Record<string, any>,
   in out TSerializedError extends Record<string, any> = Record<string, any>,
 > {
@@ -292,7 +316,12 @@ export class Router<
   __store!: Store<RouterState<TRouteTree>>
   options!: PickAsRequired<
     Omit<
-      RouterOptions<TRouteTree, TDehydrated, TSerializedError>,
+      RouterOptions<
+        TRouteTree,
+        TTrailingSlashOption,
+        TDehydrated,
+        TSerializedError
+      >,
       'transformer'
     > & {
       transformer: RouterTransformer
@@ -313,6 +342,7 @@ export class Router<
   constructor(
     options: RouterConstructorOptions<
       TRouteTree,
+      TTrailingSlashOption,
       TDehydrated,
       TSerializedError
     >,
@@ -343,6 +373,7 @@ export class Router<
   update = (
     newOptions: RouterConstructorOptions<
       TRouteTree,
+      TTrailingSlashOption,
       TDehydrated,
       TSerializedError
     >,
@@ -1848,7 +1879,13 @@ export class Router<
     TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
     TMaskTo extends string = '',
   >(
-    opts: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
+    opts: NavigateOptions<
+      Router<TRouteTree, TTrailingSlashOption, TDehydrated, TSerializedError>,
+      TFrom,
+      TTo,
+      TMaskFrom,
+      TMaskTo
+    >,
   ): Promise<Array<AnyRouteMatch> | undefined> => {
     const next = this.buildLocation(opts as any)
 
@@ -1921,7 +1958,11 @@ export class Router<
     TTo extends string = '',
     TResolved = ResolveRelativePath<TFrom, NoInfer<TTo>>,
   >(
-    location: ToOptions<TRouteTree, TFrom, TTo>,
+    location: ToOptions<
+      Router<TRouteTree, TTrailingSlashOption, TDehydrated, TSerializedError>,
+      TFrom,
+      TTo
+    >,
     opts?: MatchRouteOptions,
   ): false | RouteById<TRouteTree, TResolved>['types']['allParams'] => {
     const matchLocation = {
diff --git a/packages/react-router/src/routerContext.tsx b/packages/react-router/src/routerContext.tsx
index b5c5b7dea..c6fd19bc2 100644
--- a/packages/react-router/src/routerContext.tsx
+++ b/packages/react-router/src/routerContext.tsx
@@ -1,7 +1,7 @@
 import * as React from 'react'
 import type { Router } from './router'
 
-const routerContext = React.createContext<Router<any>>(null!)
+const routerContext = React.createContext<Router<any, any>>(null!)
 
 export function getRouterContext() {
   if (typeof document === 'undefined') {
diff --git a/packages/react-router/src/useNavigate.tsx b/packages/react-router/src/useNavigate.tsx
index f207e7f69..d60627ba0 100644
--- a/packages/react-router/src/useNavigate.tsx
+++ b/packages/react-router/src/useNavigate.tsx
@@ -3,20 +3,19 @@ import { useMatch } from './Matches'
 import { useRouter } from './useRouter'
 
 import type { NavigateOptions } from './link'
-import type { AnyRoute } from './route'
 import type { RoutePaths, RoutePathsAutoComplete } from './routeInfo'
-import type { RegisteredRouter } from './router'
+import type { AnyRouter, RegisteredRouter } from './router'
 
 export type UseNavigateResult<TDefaultFrom extends string> = <
   TTo extends string,
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = TDefaultFrom,
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = TDefaultFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
 >({
   from,
   ...rest
-}: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>) => Promise<void>
+}: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>) => Promise<void>
 
 export function useNavigate<
   TDefaultFrom extends string = string,
@@ -52,12 +51,12 @@ export function useNavigate<
 // } //
 
 export function Navigate<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TFrom extends RoutePaths<TRouteTree> | string = string,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TFrom extends RoutePaths<TRouter['routeTree']> | string = string,
   TTo extends string = '',
-  TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
+  TMaskFrom extends RoutePaths<TRouter['routeTree']> | string = TFrom,
   TMaskTo extends string = '',
->(props: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>): null {
+>(props: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>): null {
   const { navigate } = useRouter()
   const match = useMatch({ strict: false })
 
diff --git a/packages/react-router/src/useRouter.tsx b/packages/react-router/src/useRouter.tsx
index 52f4e1ab7..f179baa5d 100644
--- a/packages/react-router/src/useRouter.tsx
+++ b/packages/react-router/src/useRouter.tsx
@@ -1,12 +1,11 @@
 import * as React from 'react'
 import warning from 'tiny-warning'
 import { getRouterContext } from './routerContext'
-import type { AnyRoute } from './route'
-import type { RegisteredRouter, Router } from './router'
+import type { AnyRouter, RegisteredRouter, Router } from './router'
 
-export function useRouter<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
->(opts?: { warn?: boolean }): Router<TRouteTree> {
+export function useRouter<TRouter extends AnyRouter = RegisteredRouter>(opts?: {
+  warn?: boolean
+}): TRouter {
   const value = React.useContext(getRouterContext())
   warning(
     !((opts?.warn ?? true) && !value),
diff --git a/packages/react-router/src/useRouterState.tsx b/packages/react-router/src/useRouterState.tsx
index 5493fd94d..b27ae7d08 100644
--- a/packages/react-router/src/useRouterState.tsx
+++ b/packages/react-router/src/useRouterState.tsx
@@ -1,16 +1,15 @@
 import { useStore } from '@tanstack/react-store'
 import { useRouter } from './useRouter'
-import type { AnyRoute } from './route'
-import type { RegisteredRouter, Router, RouterState } from './router'
+import type { AnyRouter, RegisteredRouter, Router, RouterState } from './router'
 
 export function useRouterState<
-  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
-  TSelected = RouterState<TRouteTree>,
+  TRouter extends AnyRouter = RegisteredRouter,
+  TSelected = RouterState<TRouter['routeTree']>,
 >(opts?: {
-  router?: Router<TRouteTree>
+  router?: TRouter
   select: (state: RouterState<RegisteredRouter['routeTree']>) => TSelected
 }): TSelected {
-  const contextRouter = useRouter<TRouteTree>({
+  const contextRouter = useRouter<TRouter>({
     warn: opts?.router === undefined,
   })
   return useStore((opts?.router || contextRouter).__store, opts?.select as any)


Test patch:
diff --git a/packages/react-router/tests/Matches.test-d.tsx b/packages/react-router/tests/Matches.test-d.tsx
new file mode 100644
index 000000000..ed437d30e
--- /dev/null
+++ b/packages/react-router/tests/Matches.test-d.tsx
@@ -0,0 +1,68 @@
+import { expectTypeOf, test } from 'vitest'
+import {
+  createRootRoute,
+  createRoute,
+  createRouter,
+  useMatchRoute,
+} from '../src'
+
+const rootRoute = createRootRoute()
+
+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',
+  validateSearch: () => ({ page: 0 }),
+})
+
+const routeTree = rootRoute.addChildren([
+  invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+  indexRoute,
+])
+
+const defaultRouter = createRouter({
+  routeTree,
+})
+
+type DefaultRouter = typeof defaultRouter
+
+const useDefaultMatchRoute = useMatchRoute<DefaultRouter>
+
+test('when matching a route with params', () => {
+  const matchRoute = useDefaultMatchRoute()
+
+  expectTypeOf(matchRoute<string, '/invoices/$invoiceId'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | ''
+      | '/'
+      | './'
+      | '../'
+      | '/invoices'
+      | '/invoices/$invoiceId'
+      | 'invoices'
+      | 'invoices/$invoiceId'
+      | undefined
+    >()
+
+  expectTypeOf(
+    matchRoute({
+      to: '/invoices/$invoiceId',
+    }),
+  ).toEqualTypeOf<false | { invoiceId: string }>()
+})
diff --git a/packages/react-router/tests/RouterProvider.test-d.tsx b/packages/react-router/tests/RouterProvider.test-d.tsx
new file mode 100644
index 000000000..31c5d6673
--- /dev/null
+++ b/packages/react-router/tests/RouterProvider.test-d.tsx
@@ -0,0 +1,50 @@
+import { expectTypeOf, test } from 'vitest'
+import {
+  RouterProvider,
+  createRootRoute,
+  createRoute,
+  createRouter,
+} from '../src'
+
+const rootRoute = createRootRoute()
+
+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',
+  validateSearch: () => ({ page: 0 }),
+})
+
+const routeTree = rootRoute.addChildren([
+  invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+  indexRoute,
+])
+
+const defaultRouter = createRouter({
+  routeTree,
+})
+
+type DefaultRouter = typeof defaultRouter
+
+test('can pass default router to the provider', () => {
+  expectTypeOf(RouterProvider<DefaultRouter>)
+    .parameter(0)
+    .toMatchTypeOf<{
+      router: DefaultRouter
+      routeTree?: DefaultRouter['routeTree']
+    }>()
+})
diff --git a/packages/react-router/tests/link.test-d.tsx b/packages/react-router/tests/link.test-d.tsx
index a542599d1..3ea44acf3 100644
--- a/packages/react-router/tests/link.test-d.tsx
+++ b/packages/react-router/tests/link.test-d.tsx
@@ -1,5 +1,5 @@
 import { expectTypeOf, test } from 'vitest'
-import { Link, createRoute } from '../src'
+import { Link, createRoute, createRouter } from '../src'
 import { createRootRoute } from '../src'
 
 const rootRoute = createRootRoute()
@@ -68,10 +68,110 @@ const routeTree = rootRoute.addChildren([
   indexRoute,
 ])
 
-type RouteTree = typeof routeTree
+const defaultRouter = createRouter({
+  routeTree,
+})
+
+const routerAlwaysTrailingSlashes = createRouter({
+  routeTree,
+  trailingSlash: 'always',
+})
+
+const routerNeverTrailingSlashes = createRouter({
+  routeTree,
+  trailingSlash: 'never',
+})
+
+const routerPreserveTrailingSlashes = createRouter({
+  routeTree,
+  trailingSlash: 'preserve',
+})
+
+type DefaultRouter = typeof defaultRouter
+
+type RouterAlwaysTrailingSlashes = typeof routerAlwaysTrailingSlashes
+
+type RouterNeverTrailingSlashes = typeof routerNeverTrailingSlashes
+
+type RouterPreserveTrailingSlashes = typeof routerPreserveTrailingSlashes
 
 test('when navigating to the root', () => {
-  expectTypeOf(Link<RouteTree, string, '/'>)
+  expectTypeOf(Link<DefaultRouter, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../'
+      | './'
+      | ''
+      | '/'
+      | '/invoices'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/edit'
+      | '/posts'
+      | '/posts/$postId'
+      | 'invoices'
+      | 'invoices/$invoiceId'
+      | 'invoices/$invoiceId/details/$detailId'
+      | 'invoices/$invoiceId/details'
+      | 'invoices/$invoiceId/edit'
+      | 'posts'
+      | 'posts/$postId'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterAlwaysTrailingSlashes, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../'
+      | './'
+      | ''
+      | '/'
+      | '/invoices/'
+      | '/invoices/$invoiceId/'
+      | '/invoices/$invoiceId/details/$detailId/'
+      | '/invoices/$invoiceId/details/'
+      | '/invoices/$invoiceId/edit/'
+      | '/posts/'
+      | '/posts/$postId/'
+      | 'invoices/'
+      | 'invoices/$invoiceId/'
+      | 'invoices/$invoiceId/details/$detailId/'
+      | 'invoices/$invoiceId/details/'
+      | 'invoices/$invoiceId/edit/'
+      | 'posts/'
+      | 'posts/$postId/'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterNeverTrailingSlashes, string, '/'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../'
+      | './'
+      | ''
+      | '/'
+      | '/invoices'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/edit'
+      | '/posts'
+      | '/posts/$postId'
+      | 'invoices'
+      | 'invoices/$invoiceId'
+      | 'invoices/$invoiceId/details/$detailId'
+      | 'invoices/$invoiceId/details'
+      | 'invoices/$invoiceId/edit'
+      | 'posts'
+      | 'posts/$postId'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterPreserveTrailingSlashes, string, '/'>)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
@@ -79,55 +179,210 @@ test('when navigating to the root', () => {
       | './'
       | ''
       | '/'
+      | '/invoices'
       | '/invoices/'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/'
       | '/invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/edit/'
+      | '/posts'
       | '/posts/'
       | '/posts/$postId'
+      | '/posts/$postId/'
+      | 'invoices'
       | 'invoices/'
+      | 'invoices/$invoiceId'
+      | 'invoices/$invoiceId/'
       | 'invoices/$invoiceId/details/$detailId'
+      | 'invoices/$invoiceId/details/$detailId/'
+      | 'invoices/$invoiceId/details'
+      | 'invoices/$invoiceId/details/'
       | 'invoices/$invoiceId/edit'
+      | 'invoices/$invoiceId/edit/'
+      | 'posts'
       | 'posts/'
       | 'posts/$postId'
+      | 'posts/$postId/'
       | undefined
     >()
 })
 
 test('when navigating from a route with no params and no search to the root', () => {
-  expectTypeOf(Link<RouteTree, '/posts/', '/'>)
+  expectTypeOf(Link<DefaultRouter, '/posts', '/'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../'
+      | './'
+      | '/'
+      | ''
+      | '/invoices'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/$detailId'
+      | '/posts'
+      | '/posts/$postId'
+      | '$postId'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts/', '/'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../'
+      | './'
+      | '/'
+      | ''
+      | '/invoices/'
+      | '/invoices/$invoiceId/'
+      | '/invoices/$invoiceId/edit/'
+      | '/invoices/$invoiceId/details/'
+      | '/invoices/$invoiceId/details/$detailId/'
+      | '/posts/'
+      | '/posts/$postId/'
+      | '$postId/'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts/', '/'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../'
+      | './'
+      | '/'
+      | ''
+      | '/invoices'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/$detailId'
+      | '/posts'
+      | '/posts/$postId'
+      | '$postId'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts/', '/'>)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
+      | '../'
       | '../'
       | './'
       | '/'
       | ''
+      | '/invoices'
       | '/invoices/'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/'
       | '/invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/edit/'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/'
       | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/'
+      | '/posts'
       | '/posts/'
       | '/posts/$postId'
+      | '/posts/$postId/'
       | '$postId'
+      | '$postId/'
       | undefined
     >()
 })
 
 test('when navigating from a route with no params and no search to the current route', () => {
-  expectTypeOf(Link<RouteTree, '/posts/', './'>)
+  expectTypeOf(Link<DefaultRouter, '/posts/', './'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<'./$postId' | undefined | './'>()
+
+  expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts/', './'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<'./$postId/' | undefined | './'>()
+
+  expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts/', './'>)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<'./$postId' | undefined | './'>()
+
+  expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts/', './'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<'./$postId/' | './$postId' | undefined | './'>()
 })
 
 test('when navigating from a route with no params and no search to the parent route', () => {
-  expectTypeOf(Link<RouteTree, '/posts', '../'>)
+  expectTypeOf(Link<DefaultRouter, '/posts', '../'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../posts'
+      | '../posts/$postId'
+      | '../invoices/$invoiceId'
+      | '../invoices/$invoiceId/edit'
+      | '../invoices/$invoiceId/details'
+      | '../invoices/$invoiceId/details/$detailId'
+      | '../invoices'
+      | '../'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterAlwaysTrailingSlashes, '/posts', '../'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../posts/'
+      | '../posts/$postId/'
+      | '../invoices/$invoiceId/'
+      | '../invoices/$invoiceId/edit/'
+      | '../invoices/$invoiceId/details/'
+      | '../invoices/$invoiceId/details/$detailId/'
+      | '../invoices/'
+      | '../'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterNeverTrailingSlashes, '/posts', '../'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | '../posts'
+      | '../posts/$postId'
+      | '../invoices/$invoiceId'
+      | '../invoices/$invoiceId/edit'
+      | '../invoices/$invoiceId/details'
+      | '../invoices/$invoiceId/details/$detailId'
+      | '../invoices'
+      | '../'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterPreserveTrailingSlashes, '/posts', '../'>)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
+      | '../posts'
       | '../posts/'
       | '../posts/$postId'
+      | '../posts/$postId/'
+      | '../invoices/$invoiceId'
+      | '../invoices/$invoiceId/'
       | '../invoices/$invoiceId/edit'
+      | '../invoices/$invoiceId/edit/'
+      | '../invoices/$invoiceId/details'
+      | '../invoices/$invoiceId/details/'
       | '../invoices/$invoiceId/details/$detailId'
+      | '../invoices/$invoiceId/details/$detailId/'
+      | '../invoices'
       | '../invoices/'
       | '../'
       | undefined
@@ -135,30 +390,127 @@ test('when navigating from a route with no params and no search to the parent ro
 })
 
 test('cannot navigate to a branch', () => {
-  expectTypeOf(Link<RouteTree, string, '/invoices/invoiceId'>)
+  expectTypeOf(Link<DefaultRouter, string, '/invoices/$invoiceId'>)
     .parameter(0)
     .toHaveProperty('to')
     .toEqualTypeOf<
       | ''
-      | '../'
+      | '/'
+      | 'posts'
+      | '/posts'
+      | '/posts/$postId'
+      | 'posts/$postId'
+      | 'invoices'
+      | '/invoices'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/edit'
+      | 'invoices/$invoiceId'
+      | 'invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/$detailId'
+      | 'invoices/$invoiceId/details'
+      | 'invoices/$invoiceId/details/$detailId'
       | './'
+      | '../'
+      | undefined
+    >()
+
+  expectTypeOf(
+    Link<RouterAlwaysTrailingSlashes, string, '/invoices/$invoiceId'>,
+  )
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | ''
       | '/'
+      | 'posts/'
+      | '/posts/'
+      | '/posts/$postId/'
+      | 'posts/$postId/'
+      | 'invoices/'
       | '/invoices/'
-      | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/'
+      | '/invoices/$invoiceId/edit/'
+      | 'invoices/$invoiceId/'
+      | 'invoices/$invoiceId/edit/'
+      | '/invoices/$invoiceId/details/'
+      | '/invoices/$invoiceId/details/$detailId/'
+      | 'invoices/$invoiceId/details/'
+      | 'invoices/$invoiceId/details/$detailId/'
+      | './'
+      | '../'
+      | undefined
+    >()
+
+  expectTypeOf(Link<RouterNeverTrailingSlashes, string, '/invoices/$invoiceId'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | ''
+      | '/'
+      | 'posts'
+      | '/posts'
+      | '/posts/$postId'
+      | 'posts/$postId'
+      | 'invoices'
+      | '/invoices'
+      | '/invoices/$invoiceId'
       | '/invoices/$invoiceId/edit'
+      | 'invoices/$invoiceId'
+      | 'invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/$detailId'
+      | 'invoices/$invoiceId/details'
+      | 'invoices/$invoiceId/details/$detailId'
+      | './'
+      | '../'
+      | undefined
+    >()
+
+  expectTypeOf(
+    Link<RouterPreserveTrailingSlashes, string, '/invoices/$invoiceId'>,
+  )
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | ''
+      | '/'
+      | 'posts'
+      | 'posts/'
+      | '/posts'
       | '/posts/'
       | '/posts/$postId'
+      | '/posts/$postId/'
+      | 'posts/$postId'
+      | 'posts/$postId/'
+      | 'invoices'
       | 'invoices/'
-      | 'invoices/$invoiceId/details/$detailId'
+      | '/invoices'
+      | '/invoices/'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/'
+      | '/invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/edit/'
+      | 'invoices/$invoiceId'
+      | 'invoices/$invoiceId/'
       | 'invoices/$invoiceId/edit'
-      | 'posts/'
-      | 'posts/$postId'
+      | 'invoices/$invoiceId/edit/'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/'
+      | '/invoices/$invoiceId/details/$detailId'
+      | '/invoices/$invoiceId/details/$detailId/'
+      | 'invoices/$invoiceId/details'
+      | 'invoices/$invoiceId/details/'
+      | 'invoices/$invoiceId/details/$detailId'
+      | 'invoices/$invoiceId/details/$detailId/'
+      | './'
+      | '../'
       | undefined
     >()
 })
 
 test('from autocompletes to all absolute routes', () => {
-  const TestLink = Link<RouteTree, '/', '/'>
+  const TestLink = Link<DefaultRouter, '/', '/'>
   expectTypeOf(TestLink)
     .parameter(0)
     .toHaveProperty('from')
@@ -179,159 +531,355 @@ test('from autocompletes to all absolute routes', () => {
 })
 
 test('when navigating to the same route', () => {
-  const TestLink = Link<RouteTree, string, string>
+  const DefaultRouterLink = Link<DefaultRouter, string, string>
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    string
+  >
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    string
+  >
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    string,
+    string
+  >
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ params: unknown }>()
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-  expectTypeOf(TestLink)
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(DefaultRouterLink)
     .parameter(0)
     .toHaveProperty('params')
     .extract<boolean>()
     .toEqualTypeOf<true>()
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ search: unknown }>()
-
-  expectTypeOf(TestLink)
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
     .parameter(0)
-    .toHaveProperty('search')
+    .toHaveProperty('params')
     .extract<boolean>()
     .toEqualTypeOf<true>()
 
-  expectTypeOf(TestLink)
+  expectTypeOf(RouterNeverTrailingSlashesLink)
     .parameter(0)
-    .toHaveProperty('search')
+    .toHaveProperty('params')
     .extract<boolean>()
     .toEqualTypeOf<true>()
-})
-
-test('when navigating to the parent route', () => {
-  const TestLink = Link<RouteTree, string, '..'>
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ params: unknown }>()
-  expectTypeOf(TestLink)
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
     .parameter(0)
     .toHaveProperty('params')
     .extract<boolean>()
     .toEqualTypeOf<true>()
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ search: unknown }>()
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
 
-  expectTypeOf(TestLink)
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(DefaultRouterLink)
     .parameter(0)
     .toHaveProperty('search')
     .extract<boolean>()
     .toEqualTypeOf<true>()
-})
 
-test('when navigating from a route with params to the same route', () => {
-  const TestLink = Link<RouteTree, '/posts/$postId', string>
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ params: unknown }>()
-  expectTypeOf(TestLink)
+  expectTypeOf(RouterNeverTrailingSlashesLink)
     .parameter(0)
-    .toHaveProperty('params')
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
     .extract<boolean>()
     .toEqualTypeOf<true>()
 })
 
-test('when navigating to a route with params', () => {
-  const TestLink = Link<RouteTree, string, '/posts/$postId/'>
+test('when navigating to the parent route', () => {
+  const DefaultRouterLink = Link<DefaultRouter, string, '..'>
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '..'
+  >
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '..'
+  >
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '..'
+  >
 
-  expectTypeOf(TestLink).parameter(0).toMatchTypeOf<{ params: unknown }>()
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('params')
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-  params.exclude<Function | boolean>().toEqualTypeOf<{ postId: string }>()
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-  params.returns.toEqualTypeOf<{ postId: string }>()
-  params
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
     .parameter(0)
-    .toEqualTypeOf<
-      | {}
-      | { invoiceId: string }
-      | { postId: string }
-      | { invoiceId: string; detailId: string }
-    >()
-})
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-test('when navigating from a route with no params to a route with params', () => {
-  const TestLink = Link<RouteTree, '/invoices/', './$invoiceId/edit'>
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('params')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
 
-  expectTypeOf(TestLink).parameter(0).toMatchTypeOf<{ params: unknown }>()
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('params')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
 
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('params')
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('params')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
 
-  params.exclude<Function | boolean>().toEqualTypeOf<{ invoiceId: string }>()
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
 
-  params.returns.toEqualTypeOf<{ invoiceId: string }>()
-  params.parameter(0).toEqualTypeOf<{}>()
-})
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
 
-test('when navigating from a route to a route with the same params', () => {
-  const TestLink = Link<RouteTree, '/invoices/$invoiceId', './edit'>
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('params')
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ params: unknown }>()
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
 
-  params
-    .exclude<Function | boolean>()
-    .toEqualTypeOf<{ invoiceId?: string | undefined } | undefined>()
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
 
-  params.returns.toEqualTypeOf<{ invoiceId?: string | undefined }>()
-  params.parameter(0).toEqualTypeOf<{ invoiceId: string }>()
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
 })
 
-test('when navigating from a route with params to a route with different params', () => {
-  const TestLink = Link<
-    RouteTree,
-    '/invoices/$invoiceId',
-    '../../posts/$postId'
+test('when navigating from a route with params to the same route', () => {
+  const DefaultRouterLink = Link<DefaultRouter, '/posts/$postId', string>
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    '/posts/$postId',
+    string
   >
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('params')
 
-  expectTypeOf(TestLink).parameter(0).toMatchTypeOf<{ params: unknown }>()
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/posts/$postId',
+    string
+  >
 
-  params.exclude<Function | boolean>().toEqualTypeOf<{ postId: string }>()
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    '/posts/$postId',
+    string
+  >
 
-  params.returns.toEqualTypeOf<{ postId: string }>()
-  params.parameter(0).toEqualTypeOf<{ invoiceId: string }>()
-})
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-test('when navigating from a route with params to a route with an additional param', () => {
-  const TestLink = Link<
-    RouteTree,
-    '/invoices/$invoiceId',
-    './details/$detailId'
-  >
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('params')
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-  expectTypeOf(TestLink).parameter(0).toMatchTypeOf<{ params: unknown }>()
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
 
-  params
-    .exclude<Function | boolean>()
-    .toEqualTypeOf<{ invoiceId?: string | undefined; detailId: string }>()
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('params')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('params')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
 
-  params.returns.toEqualTypeOf<{ invoiceId?: string; detailId: string }>()
-  params.parameter(0).toEqualTypeOf<{ invoiceId: string }>()
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('params')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
 })
 
-test('when navigating to a union of routes with params', () => {
-  const TestLink = Link<
-    RouteTree,
+test('when navigating to a route with params', () => {
+  const DefaultRouterLink = Link<DefaultRouter, string, '/posts/$postId'>
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '/posts/$postId/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '/posts/$postId'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
     string,
-    '/invoices/$invoiceId/' | '/posts/$postId/'
+    '/posts/$postId/' | '/posts/$postId'
   >
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('params')
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ params: unknown }>()
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  const defaultRouterLinkParams = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerAlwaysTrailingSlashesLinkParams = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerNeverTrailingSlashesLinkParams = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
 
-  params
+  const routerPreserveTrailingSlashesLinkParams = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  defaultRouterLinkParams
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ invoiceId: string } | { postId: string } | undefined>()
+    .toEqualTypeOf<{ postId: string }>()
+
+  routerAlwaysTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ postId: string }>()
+
+  routerNeverTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ postId: string }>()
 
-  params.returns.toEqualTypeOf<{ invoiceId: string } | { postId: string }>()
+  routerPreserveTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ postId: string }>()
+
+  defaultRouterLinkParams.returns.toEqualTypeOf<{ postId: string }>()
+
+  routerAlwaysTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    postId: string
+  }>()
+
+  routerNeverTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    postId: string
+  }>()
 
-  params
+  routerPreserveTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    postId: string
+  }>()
+
+  defaultRouterLinkParams
     .parameter(0)
     .toEqualTypeOf<
       | {}
@@ -339,29 +887,26 @@ test('when navigating to a union of routes with params', () => {
       | { postId: string }
       | { invoiceId: string; detailId: string }
     >()
-})
-
-test('when navigating to a union of routes including the root', () => {
-  const TestLink = Link<
-    RouteTree,
-    string,
-    '/' | '/invoices/$invoiceId/' | '/posts/$postId/'
-  >
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('params')
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ params: unknown }>()
-
-  params
-    .exclude<Function | boolean>()
+  routerAlwaysTrailingSlashesLinkParams
+    .parameter(0)
     .toEqualTypeOf<
-      { invoiceId: string } | { postId: string } | {} | undefined
+      | {}
+      | { invoiceId: string }
+      | { postId: string }
+      | { invoiceId: string; detailId: string }
     >()
 
-  params.returns.toEqualTypeOf<
-    { invoiceId: string } | { postId: string } | {}
-  >()
+  routerNeverTrailingSlashesLinkParams
+    .parameter(0)
+    .toEqualTypeOf<
+      | {}
+      | { invoiceId: string }
+      | { postId: string }
+      | { invoiceId: string; detailId: string }
+    >()
 
-  params
+  routerPreserveTrailingSlashesLinkParams
     .parameter(0)
     .toEqualTypeOf<
       | {}
@@ -371,85 +916,1294 @@ test('when navigating to a union of routes including the root', () => {
     >()
 })
 
-test('when navigating from a route with search params to the same route', () => {
-  const TestLink = Link<RouteTree, '/invoices/$invoiceId', string>
+test('when navigating from a route with no params to a route with params', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    '/invoices',
+    './$invoiceId/edit'
+  >
 
-  expectTypeOf(TestLink).parameter(0).toMatchTypeOf<{ search: unknown }>()
-  expectTypeOf(TestLink)
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    '/invoices',
+    './$invoiceId/edit/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices',
+    './$invoiceId/edit'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices',
+    './$invoiceId/edit' | './invoicesId/edit/'
+  >
+
+  expectTypeOf(DefaultRouterLink)
     .parameter(0)
-    .toHaveProperty('search')
-    .extract<boolean>()
-    .toEqualTypeOf<true>()
-})
+    .toMatchTypeOf<{ params: unknown }>()
 
-test('when navigating to a route with search params', () => {
-  const TestLink = Link<RouteTree, string, '/invoices/$invoiceId/'>
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('search')
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
 
-  expectTypeOf(TestLink).parameter(0).toMatchTypeOf<{ search: unknown }>()
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
 
-  params.exclude<Function | boolean>().toEqualTypeOf<{ page: number }>()
-  params.returns.toEqualTypeOf<{ page: number }>()
-  params.parameter(0).toEqualTypeOf<{} | { page: number } | { page?: number }>()
-})
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
 
-test('when navigating to a route with optional search params', () => {
-  const TestLink = Link<RouteTree, string, '/invoices/$invoiceId/details'>
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('search')
+  const defaultRouterLinkParams = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerAlwaysTrailingSlashesLinkParams = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerNeverTrailingSlashesLinkParams = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerPreserveTrailingSlashesLinkParams = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ search: unknown }>()
+  defaultRouterLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId: string }>()
 
-  params
+  routerAlwaysTrailingSlashesLinkParams
     .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+    .toEqualTypeOf<{ invoiceId: string }>()
 
-  params.returns.toEqualTypeOf<{ page?: number }>()
-  params.parameter(0).toEqualTypeOf<{} | { page: number } | { page?: number }>()
-})
+  routerNeverTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId: string }>()
 
-test('when navigating from a route with no search params to a route with search params', () => {
-  const TestLink = Link<RouteTree, '/invoices/', './$invoiceId/'>
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('search')
+  routerPreserveTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId: string }>()
 
-  expectTypeOf(TestLink).parameter(0).toMatchTypeOf<{ search: unknown }>()
-  params.exclude<Function | boolean>().toEqualTypeOf<{ page: number }>()
-  params.returns.toEqualTypeOf<{ page: number }>()
-  params.parameter(0).toEqualTypeOf<{}>()
-})
+  defaultRouterLinkParams.returns.toEqualTypeOf<{ invoiceId: string }>()
 
-test('when navigating to a union of routes with search params', () => {
-  const TestLink = Link<
-    RouteTree,
-    string,
-    '/invoices/$invoiceId/' | '/posts/$postId/'
-  >
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('search')
+  routerAlwaysTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId: string
+  }>()
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ search: unknown }>()
+  routerNeverTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId: string
+  }>()
 
-  params
-    .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+  routerPreserveTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId: string
+  }>()
+
+  defaultRouterLinkParams.parameter(0).toEqualTypeOf<{}>()
+
+  routerAlwaysTrailingSlashesLinkParams.parameter(0).toEqualTypeOf<{}>()
 
-  params.returns.toEqualTypeOf<{ page: number } | {}>()
+  routerNeverTrailingSlashesLinkParams.parameter(0).toEqualTypeOf<{}>()
 
-  params.parameter(0).toEqualTypeOf<{} | { page: number } | { page?: number }>()
+  routerPreserveTrailingSlashesLinkParams.parameter(0).toEqualTypeOf<{}>()
 })
 
-test('when navigating to a union of routes with search params including the root', () => {
-  const TestLink = Link<
-    RouteTree,
-    string,
-    '/' | '/invoices/$invoiceId/' | '/posts/$postId/'
+test('when navigating from a route to a route with the same params', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    '/invoices/$invoiceId',
+    './edit'
   >
-  const params = expectTypeOf(TestLink).parameter(0).toHaveProperty('search')
 
-  expectTypeOf(TestLink).parameter(0).not.toMatchTypeOf<{ search: unknown }>()
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    '/invoices/$invoiceId',
+    './edit/'
+  >
 
-  params
-    .exclude<Function | boolean>()
-    .toEqualTypeOf<{ page: number } | {} | undefined>()
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices/$invoiceId',
+    './edit'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices/$invoiceId',
+    './edit' | './edit/'
+  >
+
+  const defaultRouterLinkParams = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
 
-  params.returns.toEqualTypeOf<{ page: number } | {}>()
-  params.parameter(0).toEqualTypeOf<{} | { page: number } | { page?: number }>()
+  const routerAlwaysTrailingSlashesLinkParams = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerNeverTrailingSlashesLinkParams = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerPreserveTrailingSlashesLinkParams = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  defaultRouterLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId?: string | undefined } | undefined>()
+
+  routerAlwaysTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId?: string | undefined } | undefined>()
+
+  routerNeverTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId?: string | undefined } | undefined>()
+
+  routerPreserveTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId?: string | undefined } | undefined>()
+
+  defaultRouterLinkParams.returns.toEqualTypeOf<{
+    invoiceId?: string | undefined
+  }>()
+
+  routerAlwaysTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId?: string | undefined
+  }>()
+
+  routerNeverTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId?: string | undefined
+  }>()
+
+  routerPreserveTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId?: string | undefined
+  }>()
+
+  defaultRouterLinkParams.parameter(0).toEqualTypeOf<{ invoiceId: string }>()
+
+  routerAlwaysTrailingSlashesLinkParams
+    .parameter(0)
+    .toEqualTypeOf<{ invoiceId: string }>()
+
+  routerNeverTrailingSlashesLinkParams
+    .parameter(0)
+    .toEqualTypeOf<{ invoiceId: string }>()
+
+  routerPreserveTrailingSlashesLinkParams
+    .parameter(0)
+    .toEqualTypeOf<{ invoiceId: string }>()
+})
+
+test('when navigating from a route with params to a route with different params', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    '/invoices/$invoiceId',
+    '../../posts/$postId'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    '/invoices/$invoiceId',
+    '../../posts/$postId/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices/$invoiceId',
+    '../../posts/$postId'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    '/invoices/$invoiceId',
+    '../../posts/$postId' | '../../posts/$postId/'
+  >
+
+  const defaultRouterLinkParams = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerAlwaysTrailingSlashesLinkParams = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerNeverTrailingSlashesLinkParams = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerPreserveTrailingSlashesLinkParams = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  defaultRouterLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ postId: string }>()
+
+  routerAlwaysTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ postId: string }>()
+
+  routerNeverTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ postId: string }>()
+
+  routerPreserveTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ postId: string }>()
+
+  defaultRouterLinkParams.returns.toEqualTypeOf<{ postId: string }>()
+
+  routerAlwaysTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    postId: string
+  }>()
+
+  routerNeverTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    postId: string
+  }>()
+
+  routerPreserveTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    postId: string
+  }>()
+
+  defaultRouterLinkParams.parameter(0).toEqualTypeOf<{ invoiceId: string }>()
+
+  routerAlwaysTrailingSlashesLinkParams.parameter(0).toEqualTypeOf<{
+    invoiceId: string
+  }>()
+
+  routerNeverTrailingSlashesLinkParams.parameter(0).toEqualTypeOf<{
+    invoiceId: string
+  }>()
+
+  routerPreserveTrailingSlashesLinkParams
+    .parameter(0)
+    .toEqualTypeOf<{ invoiceId: string }>()
+})
+
+test('when navigating from a route with params to a route with an additional param', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    '/invoices/$invoiceId',
+    './details/$detailId'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    '/invoices/$invoiceId',
+    './details/$detailId/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices/$invoiceId',
+    './details/$detailId'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    '/invoices/$invoiceId',
+    './details/$detailId' | './details/$detailId'
+  >
+
+  const defaultRouterLinkParams = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerAlwaysTrailingSlashesLinkParams = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerNeverTrailingSlashesLinkParams = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerPreserveTrailingSlashesLinkParams = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ params: unknown }>()
+
+  defaultRouterLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId?: string | undefined; detailId: string }>()
+
+  routerAlwaysTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId?: string | undefined; detailId: string }>()
+
+  routerNeverTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId?: string | undefined; detailId: string }>()
+
+  routerPreserveTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId?: string | undefined; detailId: string }>()
+
+  defaultRouterLinkParams.returns.toEqualTypeOf<{
+    invoiceId?: string
+    detailId: string
+  }>()
+
+  routerAlwaysTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId?: string
+    detailId: string
+  }>()
+
+  routerNeverTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId?: string
+    detailId: string
+  }>()
+
+  routerPreserveTrailingSlashesLinkParams.returns.toEqualTypeOf<{
+    invoiceId?: string
+    detailId: string
+  }>()
+})
+
+test('when navigating to a union of routes with params', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    string,
+    '/invoices/$invoiceId/edit' | '/posts/$postId'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/edit/' | '/posts/$postId/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/edit' | '/posts/$postId'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    string,
+    | '/invoices/$invoiceId/edit'
+    | '/invoices/$invoiceId/edit/'
+    | '/posts/$postId'
+    | '/posts/$postId/'
+  >
+
+  const defaultRouterLinkParams = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerAlwaysTrailingSlashesLinkParams = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerNeverTrailingSlashesLinkParams = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerPreserveTrailingSlashesLinkParams = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  defaultRouterLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId: string } | { postId: string } | undefined>()
+
+  routerAlwaysTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId: string } | { postId: string } | undefined>()
+
+  routerNeverTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId: string } | { postId: string } | undefined>()
+
+  routerPreserveTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ invoiceId: string } | { postId: string } | undefined>()
+
+  defaultRouterLinkParams.returns.toEqualTypeOf<
+    { invoiceId: string } | { postId: string }
+  >()
+
+  routerAlwaysTrailingSlashesLinkParams.returns.toEqualTypeOf<
+    { invoiceId: string } | { postId: string }
+  >()
+
+  routerNeverTrailingSlashesLinkParams.returns.toEqualTypeOf<
+    { invoiceId: string } | { postId: string }
+  >()
+
+  routerPreserveTrailingSlashesLinkParams.returns.toEqualTypeOf<
+    { invoiceId: string } | { postId: string }
+  >()
+
+  defaultRouterLinkParams
+    .parameter(0)
+    .toEqualTypeOf<
+      | {}
+      | { invoiceId: string }
+      | { postId: string }
+      | { invoiceId: string; detailId: string }
+    >()
+
+  routerAlwaysTrailingSlashesLinkParams
+    .parameter(0)
+    .toEqualTypeOf<
+      | {}
+      | { invoiceId: string }
+      | { postId: string }
+      | { invoiceId: string; detailId: string }
+    >()
+
+  routerNeverTrailingSlashesLinkParams
+    .parameter(0)
+    .toEqualTypeOf<
+      | {}
+      | { invoiceId: string }
+      | { postId: string }
+      | { invoiceId: string; detailId: string }
+    >()
+
+  routerPreserveTrailingSlashesLinkParams
+    .parameter(0)
+    .toEqualTypeOf<
+      | {}
+      | { invoiceId: string }
+      | { postId: string }
+      | { invoiceId: string; detailId: string }
+    >()
+})
+
+test('when navigating to a union of routes including the root', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    string,
+    '/' | '/invoices/$invoiceId/edit' | '/posts/$postId'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '/' | '/invoices/$invoiceId/edit/' | '/posts/$postId/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '/' | '/invoices/$invoiceId/edit' | '/posts/$postId'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    string,
+    | '/'
+    | '/invoices/$invoiceId/edit'
+    | '/invoices/$invoiceId/edit/'
+    | '/posts/$postId'
+    | '/posts/$postId/'
+  >
+
+  const defaultRouterLinkParams = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerAlwaysTrailingSlashesLinkParams = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerNeverTrailingSlashesLinkParams = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  const routerPreserveTrailingSlashesLinkParams = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('params')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ params: unknown }>()
+
+  defaultRouterLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<
+      { invoiceId: string } | { postId: string } | {} | undefined
+    >()
+
+  routerAlwaysTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<
+      { invoiceId: string } | { postId: string } | {} | undefined
+    >()
+
+  routerNeverTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<
+      { invoiceId: string } | { postId: string } | {} | undefined
+    >()
+
+  routerPreserveTrailingSlashesLinkParams
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<
+      { invoiceId: string } | { postId: string } | {} | undefined
+    >()
+
+  defaultRouterLinkParams.returns.toEqualTypeOf<
+    { invoiceId: string } | { postId: string } | {}
+  >()
+
+  routerAlwaysTrailingSlashesLinkParams.returns.toEqualTypeOf<
+    { invoiceId: string } | { postId: string } | {}
+  >()
+
+  routerNeverTrailingSlashesLinkParams.returns.toEqualTypeOf<
+    { invoiceId: string } | { postId: string } | {}
+  >()
+
+  routerPreserveTrailingSlashesLinkParams.returns.toEqualTypeOf<
+    { invoiceId: string } | { postId: string } | {}
+  >()
+
+  defaultRouterLinkParams
+    .parameter(0)
+    .toEqualTypeOf<
+      | {}
+      | { invoiceId: string }
+      | { postId: string }
+      | { invoiceId: string; detailId: string }
+    >()
+})
+
+test('when navigating from a route with search params to the same route', () => {
+  const DefaultRouterLink = Link<DefaultRouter, '/invoices/$invoiceId', string>
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    '/invoices/$invoiceId',
+    string
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices/$invoiceId',
+    string
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices/$invoiceId',
+    string
+  >
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toHaveProperty('search')
+    .extract<boolean>()
+    .toEqualTypeOf<true>()
+})
+
+test('when navigating to a route with search params', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    string,
+    '/invoices/$invoiceId/edit'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/edit/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/edit'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/edit' | '/invoices/$invoiceId/edit/'
+  >
+
+  const defaultRouterLinkSearch = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerAlwaysTrailingSlashesLinkSearch = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerNeverTrailingSlashesLinkSearch = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerPreserveTrailingSlashesLinkSearch = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  defaultRouterLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number }>()
+
+  routerAlwaysTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number }>()
+
+  routerNeverTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number }>()
+
+  routerPreserveTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number }>()
+
+  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page: number }>()
+
+  routerAlwaysTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    page: number
+  }>()
+
+  routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<{ page: number }>()
+
+  routerPreserveTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    page: number
+  }>()
+
+  defaultRouterLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerAlwaysTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerNeverTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerPreserveTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+})
+
+test('when navigating to a route with optional search params', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    string,
+    '/invoices/$invoiceId/details/$detailId'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/details/$detailId/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/details/$detailId'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    string,
+    | '/invoices/$invoiceId/details/$detailId'
+    | '/invoices/$invoiceId/details/$detailId/'
+  >
+
+  const defaultRouterLinkSearch = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerAlwaysTrailingSlashesLinkSearch = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerNeverTrailingSlashesLinkSearch = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerPreserveTrailingSlashesLinkSearch = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  defaultRouterLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+
+  routerAlwaysTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+
+  routerNeverTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+
+  routerPreserveTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page?: number | undefined } | undefined>()
+
+  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page?: number }>()
+
+  routerAlwaysTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    page?: number
+  }>()
+
+  routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    page?: number
+  }>()
+
+  routerPreserveTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    page?: number
+  }>()
+
+  defaultRouterLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerAlwaysTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerNeverTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerPreserveTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+})
+
+test('when navigating from a route with no search params to a route with search params', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    '/invoices/',
+    './$invoiceId/edit'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    '/invoices/',
+    './$invoiceId/edit/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    '/invoices/',
+    './$invoiceId/edit'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    '/invoices/',
+    './$invoiceId/edit/' | './$invoiceId/edit'
+  >
+
+  const defaultRouterLinkSearch = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerAlwaysTrailingSlashesLinkSearch = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerNeverTrailingSlashesLinkSearch = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerPreserveTrailingSlashesLinkSearch = expectTypeOf(
+    RouterPreserveTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .toMatchTypeOf<{ search: unknown }>()
+
+  defaultRouterLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number }>()
+
+  routerAlwaysTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number }>()
+
+  routerNeverTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number }>()
+
+  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page: number }>()
+
+  routerAlwaysTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    page: number
+  }>()
+
+  routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    page: number
+  }>()
+
+  routerPreserveTrailingSlashesLinkSearch.returns.toEqualTypeOf<{
+    page: number
+  }>()
+
+  defaultRouterLinkSearch.parameter(0).toEqualTypeOf<{}>()
+
+  routerAlwaysTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{}>()
+
+  routerNeverTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{}>()
+
+  routerPreserveTrailingSlashesLinkSearch.parameter(0).toEqualTypeOf<{}>()
+})
+
+test('when navigating to a union of routes with search params', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    string,
+    '/invoices/$invoiceId/edit' | '/posts/$postId'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/edit/' | '/posts/$postId/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '/invoices/$invoiceId/edit' | '/posts/$postId'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    string,
+    | '/invoices/$invoiceId/edit'
+    | '/posts/$postId'
+    | '/invoices/$invoiceId/edit/'
+    | '/posts/$postId/'
+  >
+
+  const defaultRouterLinkSearch = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerAlwaysTrailingSlashesSearch = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerNeverTrailingSlashesSearch = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerPreserveTrailingSlashesSearch = expectTypeOf(
+    RouterNeverTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  defaultRouterLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number } | {} | undefined>()
+
+  routerAlwaysTrailingSlashesSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number } | {} | undefined>()
+
+  routerNeverTrailingSlashesSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number } | {} | undefined>()
+
+  routerPreserveTrailingSlashesSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number } | {} | undefined>()
+
+  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page: number } | {}>()
+
+  routerAlwaysTrailingSlashesSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  routerNeverTrailingSlashesSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  routerPreserveTrailingSlashesSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  defaultRouterLinkSearch.returns.toEqualTypeOf<{ page: number } | {}>()
+
+  routerAlwaysTrailingSlashesSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  routerNeverTrailingSlashesSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  routerPreserveTrailingSlashesSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  defaultRouterLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerAlwaysTrailingSlashesSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerNeverTrailingSlashesSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerPreserveTrailingSlashesSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+})
+
+test('when navigating to a union of routes with search params including the root', () => {
+  const DefaultRouterLink = Link<
+    DefaultRouter,
+    string,
+    '/' | '/invoices/$invoiceId/edit' | '/posts/$postId'
+  >
+
+  const RouterAlwaysTrailingSlashesLink = Link<
+    RouterAlwaysTrailingSlashes,
+    string,
+    '/' | '/invoices/$invoiceId/edit/' | '/posts/$postId/'
+  >
+
+  const RouterNeverTrailingSlashesLink = Link<
+    RouterNeverTrailingSlashes,
+    string,
+    '/' | '/invoices/$invoiceId/edit' | '/posts/$postId'
+  >
+
+  const RouterPreserveTrailingSlashesLink = Link<
+    RouterPreserveTrailingSlashes,
+    string,
+    | '/'
+    | '/invoices/$invoiceId/edit'
+    | '/posts/$postId'
+    | '/invoices/$invoiceId/edit/'
+    | '/posts/$postId/'
+  >
+
+  const defaultRouterSearch = expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerAlwaysTrailingSlashesLinkSearch = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerNeverTrailingSlashesLinkSearch = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  const routerPreserveTrailingSlashesLinkSearch = expectTypeOf(
+    RouterAlwaysTrailingSlashesLink,
+  )
+    .parameter(0)
+    .toHaveProperty('search')
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterAlwaysTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterNeverTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  expectTypeOf(RouterPreserveTrailingSlashesLink)
+    .parameter(0)
+    .not.toMatchTypeOf<{ search: unknown }>()
+
+  defaultRouterSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number } | {} | undefined>()
+
+  routerAlwaysTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number } | {} | undefined>()
+
+  routerNeverTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number } | {} | undefined>()
+
+  routerPreserveTrailingSlashesLinkSearch
+    .exclude<Function | boolean>()
+    .toEqualTypeOf<{ page: number } | {} | undefined>()
+
+  defaultRouterSearch.returns.toEqualTypeOf<{ page: number } | {}>()
+
+  routerAlwaysTrailingSlashesLinkSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  routerNeverTrailingSlashesLinkSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  routerPreserveTrailingSlashesLinkSearch.returns.toEqualTypeOf<
+    { page: number } | {}
+  >()
+
+  defaultRouterSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerAlwaysTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerNeverTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
+
+  routerPreserveTrailingSlashesLinkSearch
+    .parameter(0)
+    .toEqualTypeOf<{} | { page: number } | { page?: number }>()
 })
diff --git a/packages/react-router/tests/redirects.test.-d.tsx b/packages/react-router/tests/redirects.test.-d.tsx
new file mode 100644
index 000000000..e962d6b9c
--- /dev/null
+++ b/packages/react-router/tests/redirects.test.-d.tsx
@@ -0,0 +1,53 @@
+import { expectTypeOf, test } from 'vitest'
+import { createRootRoute, createRoute, createRouter, redirect } from '../src'
+
+const rootRoute = createRootRoute()
+
+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',
+  validateSearch: () => ({ page: 0 }),
+})
+
+const routeTree = rootRoute.addChildren([
+  invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+  indexRoute,
+])
+
+const defaultRouter = createRouter({
+  routeTree,
+})
+
+type DefaultRouter = typeof defaultRouter
+
+test('can redirect to valid route', () => {
+  expectTypeOf(redirect<DefaultRouter, string, '/invoices'>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | ''
+      | '/'
+      | 'invoices'
+      | '/invoices'
+      | '/invoices/$invoiceId'
+      | 'invoices/$invoiceId'
+      | './'
+      | '../'
+      | undefined
+    >()
+})
diff --git a/packages/react-router/tests/useNavigate.test-d.tsx b/packages/react-router/tests/useNavigate.test-d.tsx
new file mode 100644
index 000000000..f8eb68e66
--- /dev/null
+++ b/packages/react-router/tests/useNavigate.test-d.tsx
@@ -0,0 +1,55 @@
+import { expectTypeOf, test } from 'vitest'
+import { createRootRoute, createRoute, createRouter, useNavigate } from '../src'
+
+const rootRoute = createRootRoute()
+
+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',
+  validateSearch: () => ({ page: 0 }),
+})
+
+const routeTree = rootRoute.addChildren([
+  invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+  indexRoute,
+])
+
+const defaultRouter = createRouter({
+  routeTree,
+})
+
+type DefaultRouter = typeof defaultRouter
+
+test('can redirect to valid route', () => {
+  const navigate = useNavigate()
+
+  expectTypeOf(navigate<'/invoices', DefaultRouter>)
+    .parameter(0)
+    .toHaveProperty('to')
+    .toEqualTypeOf<
+      | ''
+      | '/'
+      | 'invoices'
+      | '/invoices'
+      | '/invoices/$invoiceId'
+      | 'invoices/$invoiceId'
+      | './'
+      | '../'
+      | undefined
+    >()
+})
diff --git a/packages/react-router/tests/useRouterState.test-d.tsx b/packages/react-router/tests/useRouterState.test-d.tsx
new file mode 100644
index 000000000..39f135a3e
--- /dev/null
+++ b/packages/react-router/tests/useRouterState.test-d.tsx
@@ -0,0 +1,53 @@
+import { expectTypeOf, test } from 'vitest'
+import {
+  createRootRoute,
+  createRoute,
+  createRouter,
+  useRouterState,
+} from '../src'
+
+const rootRoute = createRootRoute({
+  validateSearch: () => ({
+    page: 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',
+  validateSearch: () => ({ page: 0 }),
+})
+
+const routeTree = rootRoute.addChildren([
+  invoicesRoute.addChildren([invoicesIndexRoute, invoiceRoute]),
+  indexRoute,
+])
+
+const defaultRouter = createRouter({
+  routeTree,
+})
+
+type DefaultRouter = typeof defaultRouter
+
+test('can select router state', () => {
+  expectTypeOf(useRouterState<DefaultRouter>)
+    .returns.toHaveProperty('location')
+    .toMatchTypeOf<{
+      search: { page?: number | undefined }
+    }>()
+})