Skip to content

match.fullPath types include a possible "" route #4892

@mkarajohn

Description

@mkarajohn

Which project does this relate to?

Router

Describe the bug

I have the exact route structure shown in the reproduction stackblitz.

I have this Breadcrumbs component (you can see it in the repo Blitzstack, it's based on the one from the kitchensink example)

import { isMatch, Link, useMatches } from '@tanstack/react-router';

export const Breadcrumbs = () => {
  const matches = useMatches();

  if (matches.some((match) => match.status === 'pending')) {
    return null;
  }

  console.log('matches', matches);

  const matchesWithCrumbs = matches.filter((match) => {
    return isMatch(match, 'context.crumb');
  });

  console.log('matchesWithCrumbs', matchesWithCrumbs);

  return (
    <nav className="grow">
      <ul className="flex items-center gap-2">
        {matchesWithCrumbs.map((match, i) => {
          console.log('match.fullPath', match.fullPath);
          console.log('match.params', match.params);

          return (
            <li key={match.id} className="flex gap-2 items-center">
              <div>
                {i + 1 < matchesWithCrumbs.length ? (
                  <Link
                    className="text-blue-700"
                    to={match.fullPath}
                    params={match.params}
                  >
                    {match.context.crumb}
                  </Link>
                ) : (
                  match.context.crumb
                )}
              </div>
              {i + 1 < matchesWithCrumbs.length ? '>' : null}
            </li>
          );
        })}
      </ul>
    </nav>
  );
};

I get this Typescript error on the Link's to prop:

Image

The type of match shows up as this:

Image

As you can see in the type of the possible fullPaths there is a "" that should not be there and it makes passing the match.fullPath throw cause it sees an unexpected route. This is not visible in the stackblitz editor, because the TS linting is off (the fullPath shows as any), but you can see it if you run it in a properly setup environment.

These are all the matching routes when we run useMatches

[
    {
        "id": "__root__",
        "index": 0,
        "routeId": "__root__",
        "params": {
            "id": "1"
        },
        "_strictParams": {},
        "pathname": "/",
        "updatedAt": 1754684906716,
        "search": {},
        "_strictSearch": {},
        "status": "success",
        "isFetching": "loader",
        "__routeContext": {},
        "context": {},
        "abortController": {},
        "fetchCount": 9,
        "cause": "stay",
        "loaderDeps": "",
        "invalid": false,
        "preload": false,
        "staticData": {},
        "loadPromise": {
            "status": "resolved"
        },
        "fullPath": "/",
        "globalNotFound": false,
        "loaderPromise": {
            "status": "resolved"
        }
    },
    {
        "id": "/_authenticated",
        "index": 1,
        "routeId": "/_authenticated",
        "params": {
            "id": "1"
        },
        "_strictParams": {},
        "pathname": "/",
        "updatedAt": 1754684913726,
        "search": {},
        "_strictSearch": {},
        "status": "success",
        "isFetching": "loader",
        "__routeContext": {},
        "__beforeLoadContext": {
            "crumb": "crumb 1"
        },
        "context": {
            "crumb": "crumb 1"
        },
        "abortController": {},
        "fetchCount": 4,
        "cause": "enter",
        "loaderDeps": "",
        "invalid": false,
        "preload": false,
        "staticData": {},
        "loadPromise": {
            "status": "resolved"
        },
        "fullPath": "/",
        "loaderPromise": {
            "status": "resolved"
        },
        "globalNotFound": false
    },
    {
        "id": "/_authenticated/something/1/",
        "index": 2,
        "routeId": "/_authenticated/something/$id/",
        "params": {
            "id": "1"
        },
        "_strictParams": {
            "id": "1"
        },
        "pathname": "/something/1/",
        "updatedAt": 1754684913726,
        "search": {},
        "_strictSearch": {},
        "status": "success",
        "isFetching": "loader",
        "__routeContext": {},
        "__beforeLoadContext": {
            "crumb": "crumb 2"
        },
        "context": {
            "crumb": "crumb 2"
        },
        "abortController": {},
        "fetchCount": 4,
        "cause": "enter",
        "loaderDeps": "",
        "invalid": false,
        "preload": false,
        "staticData": {},
        "loadPromise": {
            "status": "resolved"
        },
        "fullPath": "/something/$id/",
        "loaderPromise": {
            "status": "resolved"
        },
        "globalNotFound": false
    }
]

As you can see for /_authenticated the fullPath is "/"

Your Example Website or App

https://stackblitz.com/edit/github-1gd2n4zu?file=src%2Froutes%2F_authenticated%2Fsomething%2F%24id%2Findex.tsx

Steps to Reproduce the Bug or Issue

Try to pass the match.fullPath as value to Link's to prop

Expected behavior

The possible types for match.fullpath should not include an empty ""

Screenshots or Videos

No response

Platform

  • Router / Start Version: [e.g. 1.130.10]
  • OS: Linux
  • Browser: Chrome
  • Browser Version: 138.0.7204.92
  • Bundler: vite
  • Bundler Version: 7.0.6

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    typesChanges to the typescript types

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions