Repository: TanStack/router
Pull request #1607 (2024-05-16 21:45:05)

fix: do not allow any string in `from`
`from` technically allowed any `string` aslong as the resolved route was a valid route. Now It checks if `from` is also a valid route by itself.

This caused subtle problems when users had trailing slashes etc, the resolved route was correct but `from` was not.

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

CI is running for commit 3fcd8312af1c40d9e436b431933425228965fb43.

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


---

Sent with 💌 from [NxCloud.](https://nx.app?utm_source=pull-request&utm_medium=comment)

Patch:
diff --git a/packages/react-router/src/index.tsx b/packages/react-router/src/index.tsx
index 0b079ed6d..681cfd05a 100644
--- a/packages/react-router/src/index.tsx
+++ b/packages/react-router/src/index.tsx
@@ -183,7 +183,6 @@ export {
   type RoutesByPath,
   type RouteByPath,
   type RoutePaths,
-  type RoutePathsAutoComplete,
   type FullSearchSchema,
   type AllParams,
 } from './routeInfo'
diff --git a/packages/react-router/src/link.tsx b/packages/react-router/src/link.tsx
index 42417463a..bb00177e8 100644
--- a/packages/react-router/src/link.tsx
+++ b/packages/react-router/src/link.tsx
@@ -14,7 +14,6 @@ import type {
   RouteByPath,
   RouteByToPath,
   RoutePaths,
-  RoutePathsAutoComplete,
   RouteToPath,
 } from './routeInfo'
 import type { RegisteredRouter } from './router'
@@ -227,7 +226,7 @@ export interface ToSubOptionsProps<
   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<TRouter, TFrom> & {}
+  from?: FromPathOption<TRouter, TFrom> & {}
 }
 
 export type ParamsReducerFn<
@@ -423,6 +422,26 @@ export type ToPathOption<
       NoInfer<TTo> & string
     >
 
+export type CheckFromPath<
+  TRouter extends AnyRouter,
+  TPass,
+  TFail,
+  TFrom,
+> = string extends TFrom
+  ? TPass
+  : RouteByPath<TRouter['routeTree'], TFrom> extends never
+    ? TFail
+    : TPass
+
+export type FromPathOption<TRouter extends AnyRouter, TFrom> =
+  | CheckFromPath<
+      TRouter,
+      string extends TFrom ? TFrom & {} : TFrom,
+      never,
+      TFrom
+    >
+  | RoutePaths<TRouter['routeTree']>
+
 export interface ActiveOptions {
   exact?: boolean
   includeHash?: boolean
diff --git a/packages/react-router/src/routeInfo.ts b/packages/react-router/src/routeInfo.ts
index 3de97f2e9..f4e9d6ba5 100644
--- a/packages/react-router/src/routeInfo.ts
+++ b/packages/react-router/src/routeInfo.ts
@@ -114,10 +114,6 @@ export type RouteByToPath<TRouter extends AnyRouter, TTo> = Extract<
   AnyRoute
 >
 
-export type RoutePathsAutoComplete<TRouter extends AnyRouter, T> =
-  | (string extends T ? T & {} : T)
-  | RoutePaths<TRouter['routeTree']>
-
 // eslint-disable-next-line @typescript-eslint/naming-convention
 type UnionizeCollisions<T, U> = {
   [P in keyof T & keyof U]: T[P] extends U[P] ? T[P] : T[P] | U[P]
diff --git a/packages/react-router/src/useNavigate.tsx b/packages/react-router/src/useNavigate.tsx
index 2ae57c303..e85909ac9 100644
--- a/packages/react-router/src/useNavigate.tsx
+++ b/packages/react-router/src/useNavigate.tsx
@@ -2,8 +2,8 @@ import * as React from 'react'
 import { useMatch } from './Matches'
 import { useRouter } from './useRouter'
 
-import type { NavigateOptions } from './link'
-import type { RoutePaths, RoutePathsAutoComplete } from './routeInfo'
+import type { FromPathOption, NavigateOptions } from './link'
+import type { RoutePaths } from './routeInfo'
 import type { AnyRouter, RegisteredRouter } from './router'
 
 export type UseNavigateResult<TDefaultFrom extends string> = <
@@ -18,9 +18,10 @@ export type UseNavigateResult<TDefaultFrom extends string> = <
 }: NavigateOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>) => Promise<void>
 
 export function useNavigate<
+  TRouter extends AnyRouter = RegisteredRouter,
   TDefaultFrom extends string = string,
 >(_defaultOpts?: {
-  from?: RoutePathsAutoComplete<RegisteredRouter, TDefaultFrom>
+  from?: FromPathOption<TRouter, TDefaultFrom>
 }): UseNavigateResult<TDefaultFrom> {
   const router = useRouter()
 


Test patch:
diff --git a/packages/react-router/tests/link.test-d.tsx b/packages/react-router/tests/link.test-d.tsx
index be774e7fe..dc7b09490 100644
--- a/packages/react-router/tests/link.test-d.tsx
+++ b/packages/react-router/tests/link.test-d.tsx
@@ -533,7 +533,6 @@ test('from autocompletes to all absolute routes', () => {
       | '/'
       | '/posts/$postId'
       | '/posts/'
-      | '/'
       | '/posts'
       | '/invoices'
       | '/invoices/'
@@ -551,7 +550,45 @@ test('from autocompletes to all absolute routes', () => {
       | '/'
       | '/posts/$postId'
       | '/posts/'
+      | '/posts'
+      | '/invoices'
+      | '/invoices/'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/$detailId'
+      | undefined
+    >()
+})
+
+test('from does not allow invalid routes', () => {
+  const DefaultRouterLink = Link<DefaultRouter, '/invalid', '/'>
+  const DefaultRouterObjectsLink = Link<DefaultRouterObjects, '/invalid', '/'>
+
+  expectTypeOf(DefaultRouterLink)
+    .parameter(0)
+    .toHaveProperty('from')
+    .toEqualTypeOf<
+      | '/'
+      | '/posts/$postId'
+      | '/posts/'
+      | '/posts'
+      | '/invoices'
+      | '/invoices/'
+      | '/invoices/$invoiceId'
+      | '/invoices/$invoiceId/edit'
+      | '/invoices/$invoiceId/details'
+      | '/invoices/$invoiceId/details/$detailId'
+      | undefined
+    >()
+
+  expectTypeOf(DefaultRouterObjectsLink)
+    .parameter(0)
+    .toHaveProperty('from')
+    .toEqualTypeOf<
       | '/'
+      | '/posts/$postId'
+      | '/posts/'
       | '/posts'
       | '/invoices'
       | '/invoices/'
diff --git a/packages/react-router/tests/useNavigate.test-d.tsx b/packages/react-router/tests/useNavigate.test-d.tsx
index 9f3742000..c5d5e0150 100644
--- a/packages/react-router/tests/useNavigate.test-d.tsx
+++ b/packages/react-router/tests/useNavigate.test-d.tsx
@@ -35,7 +35,7 @@ const defaultRouter = createRouter({
 
 type DefaultRouter = typeof defaultRouter
 
-test('can redirect to valid route', () => {
+test('when navigating to a route', () => {
   const navigate = useNavigate()
 
   expectTypeOf(navigate<'/invoices', DefaultRouter>)
@@ -45,3 +45,23 @@ test('can redirect to valid route', () => {
       '/' | '/invoices' | '/invoices/$invoiceId' | './' | '../' | undefined
     >()
 })
+
+test('when setting a default from', () => {
+  expectTypeOf(useNavigate<DefaultRouter, '/'>)
+    .parameter(0)
+    .exclude<undefined>()
+    .toHaveProperty('from')
+    .toEqualTypeOf<
+      '/invoices' | '/' | '/invoices/$invoiceId' | '/invoices/' | undefined
+    >()
+})
+
+test('when setting an invalid default from', () => {
+  expectTypeOf(useNavigate<DefaultRouter, '/invalid'>)
+    .parameter(0)
+    .exclude<undefined>()
+    .toHaveProperty('from')
+    .toEqualTypeOf<
+      '/invoices' | '/' | '/invoices/$invoiceId' | '/invoices/' | undefined
+    >()
+})