Repository: TanStack/router (ts)

Issue:
Path parameter with value 0 is ignored
### Describe the bug

`<Link
          to={'/employee/$id'}
          params={{
            id: 0,
          }}
        >
          Employee with id 0
        </Link>`

With route:

`export const Route = createFileRoute('/employee/$id')({
  component: AboutComponent,
});`

Since a recent update (1.38.1 -> 1.45.10), this link generates: `/employee` while it used to generate `/employee/0`


`<Link
          to={'/employee/$id'}
          params={{
            id: 5,
          }}
        >
          Employee with id 5
        </Link>`
Still correctly generates  `/employee/5`



### Your Example Website or App

https://stackblitz.com/edit/tanstack-router-jnrhtc?file=src%2Froutes%2F__root.tsx

### Steps to Reproduce the Bug or Issue

- Click on 'About with id 0'
- Notice it renders "Not found" because the link does not include the parameter

### Expected behavior

Path parameters should support the number 0 as we use index based routing in a List->Detail view for entities that don't have a uuid.

### Screenshots or Videos

_No response_

### Platform

- Browser: Chrome



### Additional context

Why I need this:
I use react-hook-form's fieldarray to let the user modifiy an array of 'things'. When the user clicks the 'add thing'
 button I add a 'thing' and navigate to /mypage/things/0 to let the user enter the thing's details.

Pull request:
fix(react-router): retain path parameters that are falsy but still valid 
Fixes #2024 

Patch:
diff --git a/packages/react-router/src/path.ts b/packages/react-router/src/path.ts
index cd936ce2b..46a79bd0e 100644
--- a/packages/react-router/src/path.ts
+++ b/packages/react-router/src/path.ts
@@ -8,7 +8,13 @@ export interface Segment {
 }
 
 export function joinPaths(paths: Array<string | undefined>) {
-  return cleanPath(paths.filter(Boolean).join('/'))
+  return cleanPath(
+    paths
+      .filter((val) => {
+        return val !== undefined
+      })
+      .join('/'),
+  )
 }
 
 export function cleanPath(path: string) {
@@ -193,7 +199,7 @@ export function parsePathname(pathname?: string): Array<Segment> {
 
 interface InterpolatePathOptions {
   path?: string
-  params: any
+  params: Record<string, unknown>
   leaveWildcards?: boolean
   leaveParams?: boolean
 }


Test patch:
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index c7e319e9e..8e3604ebb 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -3293,6 +3293,73 @@ describe('Link', () => {
 
     expect(parseParams).toHaveBeenCalledWith({ status: 'parsed', postId: '2' })
   })
+
+  test('when navigating to /$postId with params.parse and params.stringify handles falsey inputs', async () => {
+    const rootRoute = createRootRoute()
+
+    const indexRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '/',
+      component: () => (
+        <>
+          <Link to="/$postId" params={{ postId: 2 }}>
+            Go to post 2
+          </Link>
+          <Link to="/$postId" params={{ postId: 0 }}>
+            Go to post 0
+          </Link>
+        </>
+      ),
+    })
+
+    const stringifyParamsMock = vi.fn()
+
+    const parseParams = ({ postId }: { postId: string }) => {
+      return {
+        postId: parseInt(postId),
+      }
+    }
+
+    const stringifyParams = ({ postId }: { postId: number }) => {
+      stringifyParamsMock({ postId })
+      return {
+        postId: postId.toString(),
+      }
+    }
+
+    const PostComponent = () => {
+      const params = useParams({ strict: false })
+      return <div>Post: {params.postId}</div>
+    }
+
+    const postRoute = createRoute({
+      getParentRoute: () => rootRoute,
+      path: '$postId',
+      params: {
+        parse: parseParams,
+        stringify: stringifyParams,
+      },
+      component: PostComponent,
+    })
+
+    const routeTree = rootRoute.addChildren([indexRoute, postRoute])
+    const router = createRouter({ routeTree })
+
+    render(<RouterProvider router={router} />)
+
+    const postLink2 = await screen.findByRole('link', {
+      name: 'Go to post 2',
+    })
+    const postLink0 = await screen.findByRole('link', {
+      name: 'Go to post 0',
+    })
+
+    expect(postLink2).toHaveAttribute('href', '/2')
+    expect(postLink0).toHaveAttribute('href', '/0')
+
+    expect(stringifyParamsMock).toHaveBeenCalledWith({ postId: 2 })
+    expect(stringifyParamsMock).toHaveBeenCalledWith({ postId: 0 })
+  })
 })
 
 describe('createLink', () => {
diff --git a/packages/react-router/tests/path.test.ts b/packages/react-router/tests/path.test.ts
index d018a4a2d..d77719c2a 100644
--- a/packages/react-router/tests/path.test.ts
+++ b/packages/react-router/tests/path.test.ts
@@ -1,8 +1,7 @@
-import exp from 'node:constants'
 import { describe, expect, it } from 'vitest'
 import {
   exactPathTest,
-  matchByPath,
+  interpolatePath,
   removeBasepath,
   removeTrailingSlash,
   resolvePath,
@@ -225,3 +224,55 @@ describe('resolvePath', () => {
     })
   })
 })
+
+describe('interpolatePath', () => {
+  ;[
+    {
+      name: 'should interpolate the path',
+      path: '/users/$id',
+      params: { id: '123' },
+      result: '/users/123',
+    },
+    {
+      name: 'should interpolate the path with multiple params',
+      path: '/users/$id/$name',
+      params: { id: '123', name: 'tanner' },
+      result: '/users/123/tanner',
+    },
+    {
+      name: 'should interpolate the path with extra params',
+      path: '/users/$id',
+      params: { id: '123', name: 'tanner' },
+      result: '/users/123',
+    },
+    {
+      name: 'should interpolate the path with missing params',
+      path: '/users/$id/$name',
+      params: { id: '123' },
+      result: '/users/123/undefined',
+    },
+    {
+      name: 'should interpolate the path with missing params and extra params',
+      path: '/users/$id',
+      params: { name: 'john' },
+      result: '/users/undefined',
+    },
+    {
+      name: 'should interpolate the path with the param being a number',
+      path: '/users/$id',
+      params: { id: 123 },
+      result: '/users/123',
+    },
+    {
+      name: 'should interpolate the path with the param being a falsey number',
+      path: '/users/$id',
+      params: { id: 0 },
+      result: '/users/0',
+    },
+  ].forEach((exp) => {
+    it(exp.name, () => {
+      const result = interpolatePath({ path: exp.path, params: exp.params })
+      expect(result).toBe(exp.result)
+    })
+  })
+})