Repository: TanStack/router
Pull request #2659 (2024-10-27 21:18:13)

fix(react-router): correctly narrow in `router.invalidate` and `router.clearCache`

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

CI is running for commit 5828c2a4a225357bd7e0b285dfc98c91df967eaf.

[📂 Click to track the progress, see the status, the terminal output, and the build insights.](https://cloud.nx.app/cipes/671eae33cb92a3052ea0cd69?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/docs/framework/react/api/router/RouterType.md b/docs/framework/react/api/router/RouterType.md
index 7250f3d30..22c22d38f 100644
--- a/docs/framework/react/api/router/RouterType.md
+++ b/docs/framework/react/api/router/RouterType.md
@@ -131,17 +131,17 @@ Navigates to a new location.
 
 Invalidates route matches by forcing their `beforeLoad` and `load` functions to be called again.
 
-- Type: `(opts?: {filter?: (d: MakeRouteMatch<TRouteTree>) => boolean}) => Promise<void>`
+- Type: `(opts?: {filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean}) => Promise<void>`
 - This is useful any time your loader data might be out of date or stale. For example, if you have a route that displays a list of posts, and you have a loader function that fetches the list of posts from an API, you might want to invalidate the route matches for that route any time a new post is created so that the list of posts is always up-to-date.
 - if `filter` is not supplied, all matches will be invalidated
 - if `filter` is supplied, only matches for which `filter` returns `true` will be invalidated.
 - You might also want to invalidate the Router if you imperatively `reset` the router's `CatchBoundary` to trigger loaders again.
 
-### `.removeCachedMatches` method
+### `.clearCache` method
 
 Remove cached route matches.
 
-- Type: `(opts?: {filter?: (d: MakeRouteMatch<TRouteTree>) => boolean}) => void`
+- Type: `(opts?: {filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean}) => void`
 - if `filter` is not supplied, all cached matches will be removed
 - if `filter` is supplied, only matches for which `filter` returns `true` will be removed.
 
diff --git a/packages/react-router/src/router.ts b/packages/react-router/src/router.ts
index 3dda5bece..e7bdec9d2 100644
--- a/packages/react-router/src/router.ts
+++ b/packages/react-router/src/router.ts
@@ -70,6 +70,7 @@ import type {
 import type {
   AnyRouteMatch,
   MakeRouteMatch,
+  MakeRouteMatchUnion,
   MatchRouteOptions,
 } from './Matches'
 import type { ParsedLocation } from './location'
@@ -2553,11 +2554,11 @@ export class Router<
     return matches
   }
 
-  invalidate = (opts?: {
-    filter?: (d: MakeRouteMatch<TRouteTree>) => boolean
+  invalidate = <TRouter extends AnyRouter = RegisteredRouter>(opts?: {
+    filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
   }) => {
     const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
-      if (opts?.filter?.(d) ?? true) {
+      if (opts?.filter?.(d as MakeRouteMatchUnion<TRouter>) ?? true) {
         return {
           ...d,
           invalid: true,
@@ -2589,15 +2590,17 @@ export class Router<
     return redirect
   }
 
-  clearCache = (opts?: {
-    filter?: (d: MakeRouteMatch<TRouteTree>) => boolean
+  clearCache = <TRouter extends AnyRouter = RegisteredRouter>(opts?: {
+    filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
   }) => {
     const filter = opts?.filter
     if (filter !== undefined) {
       this.__store.setState((s) => {
         return {
           ...s,
-          cachedMatches: s.cachedMatches.filter((m) => !filter(m)),
+          cachedMatches: s.cachedMatches.filter(
+            (m) => !filter(m as MakeRouteMatchUnion<TRouter>),
+          ),
         }
       })
     } else {


Test patch:
diff --git a/packages/react-router/tests/router.test-d.tsx b/packages/react-router/tests/router.test-d.tsx
index 8d565b805..9e3471062 100644
--- a/packages/react-router/tests/router.test-d.tsx
+++ b/packages/react-router/tests/router.test-d.tsx
@@ -146,3 +146,84 @@ test('when creating a router with context and children', () => {
       context: { userId: string }
     }>()
 })
+
+test('invalidate and clearCache narrowing in filter', () => {
+  const rootRoute = createRootRoute()
+
+  const invoicesRoute = createRoute({
+    path: 'invoices',
+    getParentRoute: () => rootRoute,
+    validateSearch: () => ({ page: 0 }),
+    beforeLoad: () => ({ invoicePermissions: ['view'] as const }),
+  })
+
+  const invoiceRoute = createRoute({
+    path: '$invoiceId',
+    getParentRoute: () => invoicesRoute,
+  })
+
+  const detailsRoute = createRoute({
+    path: 'details',
+    getParentRoute: () => invoiceRoute,
+    validateSearch: () => ({ detailPage: 0 }),
+    beforeLoad: () => ({ detailsPermissions: ['view'] as const }),
+  })
+
+  const detailRoute = createRoute({
+    path: '$detailId',
+    getParentRoute: () => detailsRoute,
+  })
+
+  const routeTree = rootRoute.addChildren([
+    invoicesRoute.addChildren([
+      invoiceRoute.addChildren([detailsRoute.addChildren([detailRoute])]),
+    ]),
+  ])
+
+  const router = createRouter({
+    routeTree,
+    context: { userId: 'userId' },
+  })
+
+  type Router = typeof router
+
+  router.invalidate<Router>({
+    filter: (route) => {
+      expectTypeOf(route.routeId).toEqualTypeOf<
+        | '__root__'
+        | '/invoices'
+        | '/invoices/$invoiceId'
+        | '/invoices/$invoiceId/details'
+        | '/invoices/$invoiceId/details/$detailId'
+      >()
+
+      if (route.routeId === '/invoices/$invoiceId/details/$detailId') {
+        expectTypeOf(route.params).branded.toEqualTypeOf<{
+          invoiceId: string
+          detailId: string
+        }>()
+      }
+      return true
+    },
+  })
+
+  router.clearCache<Router>({
+    filter: (route) => {
+      expectTypeOf(route.routeId).toEqualTypeOf<
+        | '__root__'
+        | '/invoices'
+        | '/invoices/$invoiceId'
+        | '/invoices/$invoiceId/details'
+        | '/invoices/$invoiceId/details/$detailId'
+      >()
+
+      if (route.routeId === '/invoices/$invoiceId/details/$detailId') {
+        expectTypeOf(route.params).branded.toEqualTypeOf<{
+          invoiceId: string
+          detailId: string
+        }>()
+      }
+      return true
+    },
+  })
+})