Skip to content

Conversation

@devel-maverick
Copy link

@devel-maverick devel-maverick commented Dec 3, 2025

Description

This PR fixes a TypeScript inference issue where route parameters are not detected when a route file is placed inside a _pathless directory. Runtime behavior was already correct, but TypeScript always inferred router.matchRoute() as false, causing parameter access to fail at compile time.

Problem

When using a structure like:

routes/_pathless/nested/$id.tsx

the type-level path becomes:

"/_pathless/nested/$id"

Even though _pathless is removed from the actual URL at runtime, the type system still sees it as a real segment. This prevents $id from being extracted, producing:

const match = router.matchRoute({ 
  to: "/nested/$id", 
  params: { id: "123" } 
})

// match inferred as false only
match?.id // Property 'id' does not exist

Fix

We normalize the type-level path by stripping _pathless before param extraction:

StripPathless<TFrom>, StripPathless<TTo>

Then apply this normalization inside ValidateParams:

export type ValidateParams<
  TRouter extends AnyRouter = RegisteredRouter,
  TTo extends string | undefined = undefined,
  TFrom extends string = string,
> = PathParamOptions<
  TRouter,
  StripPathless<TFrom>,
  TTo extends string ? StripPathless<TTo> : TTo
>

This ensures TypeScript sees the correct path:

"/nested/$id"

allowing parameter extraction to work as intended.

Result (after fix)

const match = router.matchRoute({
  to: "/nested/$id",
  params: { id: "123" }
})

// ✔ match: false | { id: string }
match?.id // correctly typed

fixes: (#6011)

Summary by CodeRabbit

  • Improvements
    • Path validation in the router now features automatic normalization of special routing segments for consistent and reliable behavior. This enhancement reduces configuration edge cases, improves routing predictability, and ensures stable handling across complex scenarios with multiple parameters and nested paths, making the routing system more robust and maintainable.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 3, 2025

Walkthrough

Introduces a new StripPathless type utility that recursively removes _pathless folder segments from path strings and updates ValidateParams to normalize both source and destination paths before type validation, eliminating patterns like A/_pathless/B to A/B.

Changes

Cohort / File(s) Summary
Type utility enhancement
packages/router-core/src/typePrimitives.ts
Adds StripPathless<T> type that removes _pathless segments from path strings using recursive conditional types; updates ValidateParams to apply StripPathless to both TFrom and TTo parameters for path normalization before passing to PathParamOptions

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

  • Single-file, type-only change with straightforward recursive conditional type logic
  • No runtime behavior modifications or control flow changes
  • Clear and self-contained utility with minimal surface area

Possibly related PRs

  • TanStack/router#6015: Addresses complementary handling of pathless layouts and param extraction; one at the type level and one at runtime.

Suggested reviewers

  • schiller-manuel
  • nlynzaad

Poem

🐰 Pathless folders hiding in the trees,
_Stripped away with such gentle ease,
Types now pure, the path runs clean,
No tangled \_ between!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing TypeScript parameter inference for routes in _pathless directories, which matches the core purpose of introducing the StripPathless type and updating ValidateParams.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34d13e5 and 4cae1ae.

📒 Files selected for processing (1)
  • packages/router-core/src/typePrimitives.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety throughout the codebase

Files:

  • packages/router-core/src/typePrimitives.ts
🧠 Learnings (7)
📓 Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to packages/solid-router/**/*.{ts,tsx} : Solid Router components and primitives should use the tanstack/solid-router package
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to **/src/routes/**/*.{ts,tsx} : Use file-based routing in src/routes/ directories or code-based routing with route definitions
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.
📚 Learning: 2025-09-22T00:56:49.237Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.

Applied to files:

  • packages/router-core/src/typePrimitives.ts
📚 Learning: 2025-09-22T00:56:53.426Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.

Applied to files:

  • packages/router-core/src/typePrimitives.ts
📚 Learning: 2025-11-25T00:18:21.282Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to packages/solid-router/**/*.{ts,tsx} : Solid Router components and primitives should use the tanstack/solid-router package

Applied to files:

  • packages/router-core/src/typePrimitives.ts
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • packages/router-core/src/typePrimitives.ts
📚 Learning: 2025-11-25T00:18:21.282Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-11-25T00:18:21.282Z
Learning: Applies to **/src/routes/**/*.{ts,tsx} : Use file-based routing in src/routes/ directories or code-based routing with route definitions

Applied to files:

  • packages/router-core/src/typePrimitives.ts
📚 Learning: 2025-10-14T18:59:33.990Z
Learnt from: FatahChan
Repo: TanStack/router PR: 5475
File: e2e/react-start/basic-prerendering/src/routes/redirect/$target/via-beforeLoad.tsx:8-0
Timestamp: 2025-10-14T18:59:33.990Z
Learning: In TanStack Router e2e test files, when a route parameter is validated at the route level (e.g., using zod in validateSearch or param validation), switch statements on that parameter do not require a default case, as the validation ensures only expected values will reach the switch.

Applied to files:

  • packages/router-core/src/typePrimitives.ts
🔇 Additional comments (1)
packages/router-core/src/typePrimitives.ts (1)

42-46: Good targeted normalization of _pathless for parameter validation

Applying StripPathless to both TFrom and (when a string) TTo in ValidateParams is a clean way to align type-level param extraction with the runtime behavior for _pathless directories, and the defaults still reduce to the previous PathParamOptions<TRouter, string, undefined> shape when generics aren’t specified.

If, in the future, you find similar inference gaps for other helpers (ValidateSearch, ValidateToPath, etc.), you might consider reusing StripPathless there as well for consistency, but what you have here is sufficient for the reported issue.

Comment on lines +14 to +18
// Remove any `_pathless` folder segments for type extraction
export type StripPathless<T extends string> =
T extends `${infer A}/_pathless/${infer B}`
? `${A}/${B}`
: T;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

StripPathless only removes the first _pathless segment; consider making it recursive

As written, StripPathless stops after stripping the first /_pathless/ occurrence, so paths with multiple _pathless folders (e.g. "/foo/_pathless/bar/_pathless/baz") will still contain a _pathless segment and won’t be fully normalized for param extraction.

You can make this handle all occurrences with a small recursive change:

-export type StripPathless<T extends string> =
-  T extends `${infer A}/_pathless/${infer B}`
-    ? `${A}/${B}`
-    : T;
+export type StripPathless<T extends string> =
+  T extends `${infer A}/_pathless/${infer B}`
+    ? StripPathless<`${A}/${B}`>
+    : T;

This preserves behavior for simple cases like "/_pathless/nested/$id" while correctly normalizing any deeper _pathless nesting. Based on learnings, this keeps the special handling scoped to the _pathless folder name without affecting other underscore semantics.

🤖 Prompt for AI Agents
In packages/router-core/src/typePrimitives.ts around lines 14 to 18,
StripPathless currently only removes the first "/_pathless/" segment; change it
to be recursive so all occurrences are stripped. Replace the conditional branch
so that when T extends `${infer A}/_pathless/${infer B}` you return
StripPathless<`${A}/${B}`> (rather than just `${A}/${B}`) so nested/multiple
_pathless segments are normalized; keep the fallback as T.

@schiller-manuel
Copy link
Contributor

very likely we need the same fix as in #1690

we also need type tests

@devel-maverick
Copy link
Author

Thanks! Yes — I can mirror the approach from #1690.

I'll update this PR to:

Apply the same normalization logic inside useMatchRoute for layout routes

Add the missing type tests to ensure StripPathless behaves correctly, including nested cases.

Also included the recursive StripPathless fix suggested by CodeRabbit so multiple _pathless segments are fully stripped.

Let me push an update shortly.

@schiller-manuel
Copy link
Contributor

we wouldn't need any additional typescript types it the other fix works out

@devel-maverick
Copy link
Author

Hey! Just checking in —
Should I wait for the other related fix to land first before updating this PR?
Or is this one good to proceed independently?

Happy to change the implementation either way. Let me know if there's anything blocking the merge, since I can update this PR pretty quickly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants