Skip to content

fix(router-core): fix search middleware type to use schema output#7381

Merged
schiller-manuel merged 4 commits into
TanStack:mainfrom
sprusr:main
Jun 5, 2026
Merged

fix(router-core): fix search middleware type to use schema output#7381
schiller-manuel merged 4 commits into
TanStack:mainfrom
sprusr:main

Conversation

@sprusr

@sprusr sprusr commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

The type for search passed to a route's search middleware is currently set to the input of whatever is passed to validateSearch. In reality this value is the output of the search validator.

In the majority of cases these types are the same, but it becomes a pronounced issue with asymmetric schemas, for example with Zod codecs:

// input is number, output is BigInt
const numberToBigInt = z.codec(z.int(), z.bigint(), {
  decode: (num) => BigInt(num),
  encode: (bigint) => Number(bigint),
});

const searchSchema = z.object({
  codec: numberToBigInt.optional(),
})

The value that we get in the search middleware is a BigInt but it's typed as a number.

Changes

Simple switch from ResolveFullSearchSchemaInput to ResolveFullSearchSchema.

Reproduction

You can find here a small TanStack Start project with a demo of the mismatch: https://github.com/sprusr/tanstack-router-search-middleware-types

Summary by CodeRabbit

  • Refactor
    • Search middleware now consistently receives the validated search shape (middleware input types aligned with validator output).
    • Helper that strips defaulted search parameters accepts validator-shaped defaults for consistent typing.
  • Tests
    • Added type-level tests to verify middleware and validators produce matching search shapes and stripping behavior.
  • Chores
    • Recorded a patch changeset for the router-core package.

@github-actions

Copy link
Copy Markdown
Contributor

Bundle Size Benchmarks

  • Commit: b1c061aff918
  • Measured at: 2026-05-11T16:48:47.955Z
  • Baseline source: history:b1c061aff918
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.29 KiB 0 B (0.00%) 87.15 KiB 274.07 KiB 75.85 KiB ▁▁▁▁▁▁▁▁▁██
react-router.full 90.82 KiB 0 B (0.00%) 90.68 KiB 285.58 KiB 78.89 KiB ▁▁▁▁▁▁▁▁▁██
solid-router.minimal 35.51 KiB 0 B (0.00%) 35.39 KiB 106.36 KiB 31.97 KiB ▁▁▁▁▁▁▁▁▁██
solid-router.full 40.23 KiB 0 B (0.00%) 40.10 KiB 120.58 KiB 36.14 KiB ▁▁▁▁▁▁▁▁▁██
vue-router.minimal 53.29 KiB 0 B (0.00%) 53.15 KiB 151.51 KiB 47.85 KiB ▁▁▁▁▁▁▁▁▁██
vue-router.full 58.41 KiB 0 B (0.00%) 58.28 KiB 167.69 KiB 52.39 KiB ▁▁▁▁▁▁▁▁▁██
react-start.minimal 101.97 KiB 0 B (0.00%) 101.83 KiB 322.51 KiB 88.17 KiB ▁▁▁▁▁▁▁▁▃██
react-start.full 105.41 KiB 0 B (0.00%) 105.27 KiB 332.84 KiB 91.19 KiB ▁▁▁▁▁▁▁▁▄██
react-start.rsbuild.minimal 99.60 KiB 0 B (0.00%) 99.43 KiB 316.97 KiB 85.64 KiB ▁▁▁▁▁▁▁▁▄██
react-start.rsbuild.full 102.89 KiB 0 B (0.00%) 102.72 KiB 327.41 KiB 88.45 KiB ▁▁▁▁▁▁▁▁▃██
solid-start.minimal 49.61 KiB 0 B (0.00%) 49.48 KiB 152.48 KiB 43.78 KiB ▁▁▁▁▁▁▁▁▄██
solid-start.full 55.40 KiB 0 B (0.00%) 55.27 KiB 169.39 KiB 48.72 KiB ▁▁▁▁▁▁▁▁▄██

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Need the big picture first? Review this PR in Change Stack to see what changed before going file by file.

Review Change Stack

📝 Walkthrough

Walkthrough

The search middleware generic in UpdatableRouteOptions now uses the resolved search validator output type (ResolveFullSearchSchema) instead of the input type; stripSearchParams typing was adjusted and TypeScript declaration tests were added. A changeset entry was added.

Changes

Search Middleware Type Update

Layer / File(s) Summary
Search middleware annotation
packages/router-core/src/route.ts
UpdatableRouteOptions.search.middlewares now uses SearchMiddleware<ResolveFullSearchSchema<TParentRoute, TSearchValidator>>.
stripSearchParams typing
packages/router-core/src/searchMiddleware.ts
stripSearchParams TValues generic now accepts Partial<NoInfer<TSearchSchema>> or Array<keyof TOptionalProps>.
Type-level tests for middleware and strip
packages/router-core/tests/searchMiddleware.test-d.ts
Adds declaration tests asserting middleware search/next infer the validator's SearchOutput and that stripSearchParams removes default-valued keys.
Release notes (changeset)
.changeset/quiet-plums-help.md
Adds a patch changeset for @tanstack/router-core noting the search middleware now uses the validator output type.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A tiny hop in router land,
Types realigned by careful hand,
Middleware greets the output true,
Validators whisper what to do,
I nibble carrots, stamp my paw — hooray for compile-time law!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: fixing search middleware type to use schema output instead of schema input, which is the core purpose of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@sprusr

sprusr commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

Long time TanStack Router fan, first time contributor. I spent a bit of time validating my theory with this one, so hopefully haven't jumped to the wrong conclusion.

As an aside, I would love to see more comprehensive support of asymmetrical schemas particularly when it comes to search params. An example use case: I'm unhappy with the appearance of how arrays of strings are encoded in the url by default, and would prefer to have them as space-separated values - being able to do this with Zod codecs would be wonderful (I am already, but it relies on own middleware).

@sprusr

sprusr commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

In fact if maintainers agree with conclusions here then I think buildLocation has wrong sided types too, which affects Link and navigate at least.

@schiller-manuel schiller-manuel requested a review from chorobin May 12, 2026 12:32
@sprusr

sprusr commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

@chorobin wondering if you'd have a chance to take a look at this. Not sure what the situation is now with external contributors, but it would be great to have this types issue fixed in some form or another. Thank you!

@schiller-manuel

Copy link
Copy Markdown
Collaborator

can you please add a type test?

@nx-cloud

nx-cloud Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 84e83ce

Command Status Duration Result
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2m 21s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-05 20:00:26 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jun 5, 2026

Copy link
Copy Markdown
More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7381

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7381

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7381

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7381

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7381

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7381

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7381

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7381

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7381

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7381

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7381

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7381

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7381

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7381

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7381

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7381

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7381

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7381

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7381

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7381

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7381

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7381

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7381

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7381

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7381

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7381

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7381

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7381

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7381

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7381

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7381

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7381

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7381

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7381

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7381

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7381

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7381

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7381

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7381

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7381

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7381

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7381

commit: a40df23

@codspeed-hq

codspeed-hq Bot commented Jun 5, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing sprusr:main (84e83ce) with main (301f6ba)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Important

At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.

Nx Cloud is proposing a fix for your failed CI:

We changed stripSearchParams's TValues type from Partial<PickOptional<TSearchSchema>> to Partial<TSearchSchema> so that default values can be provided for required fields — which is the case when a Zod schema uses .default(), making the field required in the output type. This unblocks stripSearchParams({ foo: 'default' }) usage after the middleware context was correctly changed to use the schema output type instead of the input type. The outdated comment noting this as a known limitation has also been removed from the affected test files.

Tip

We verified this fix by re-running @tanstack/vue-router:test:types, @tanstack/react-router:test:unit, @tanstack/solid-router:test:unit and 1 more.

Suggested Fix changes
diff --git a/packages/react-router/tests/link.test.tsx b/packages/react-router/tests/link.test.tsx
index f06a3a8d..ec74e6f1 100644
--- a/packages/react-router/tests/link.test.tsx
+++ b/packages/react-router/tests/link.test.tsx
@@ -5891,7 +5891,6 @@ describe('search middleware', () => {
       }),
       search: {
         middlewares: [
-          // this means we cannot get the correct input type for this schema
           stripSearchParams({ foo: 'default' }),
           retainSearchParams(true),
         ],
diff --git a/packages/router-core/src/searchMiddleware.ts b/packages/router-core/src/searchMiddleware.ts
index a564650a..b8ac1a73 100644
--- a/packages/router-core/src/searchMiddleware.ts
+++ b/packages/router-core/src/searchMiddleware.ts
@@ -82,9 +82,7 @@ function getValidationDefaultKeys(
 export function stripSearchParams<
   TSearchSchema,
   TOptionalProps = PickOptional<NoInfer<TSearchSchema>>,
-  const TValues =
-    | Partial<NoInfer<TOptionalProps>>
-    | Array<keyof TOptionalProps>,
+  const TValues = Partial<NoInfer<TSearchSchema>> | Array<keyof TOptionalProps>,
   const TInput = IsRequiredParams<TSearchSchema> extends never
     ? TValues | true
     : TValues,
diff --git a/packages/solid-router/tests/link.test.tsx b/packages/solid-router/tests/link.test.tsx
index d0fc7ae4..03f71805 100644
--- a/packages/solid-router/tests/link.test.tsx
+++ b/packages/solid-router/tests/link.test.tsx
@@ -5762,7 +5762,6 @@ describe('search middleware', () => {
       }),
       search: {
         middlewares: [
-          // this means we cannot get the correct input type for this schema
           stripSearchParams({ foo: 'default' }),
           retainSearchParams(true),
         ],
diff --git a/packages/vue-router/tests/link.test.tsx b/packages/vue-router/tests/link.test.tsx
index e047fe59..ed223135 100644
--- a/packages/vue-router/tests/link.test.tsx
+++ b/packages/vue-router/tests/link.test.tsx
@@ -5933,7 +5933,6 @@ describe('search middleware', () => {
       }),
       search: {
         middlewares: [
-          // this means we cannot get the correct input type for this schema
           stripSearchParams({ foo: 'default' }),
           retainSearchParams(true),
         ],

Because this branch comes from a fork, it is not possible for us to apply fixes directly, but you can apply the changes locally using the available options below.

Apply changes locally with:

npx nx-cloud apply-locally ilK4-KXCt

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/router-core/src/searchMiddleware.ts (1)

71-71: 💤 Low value

Existing type cast reduces return type safety.

The as any cast bypasses TypeScript's type checking on the middleware's return value. Combined with the widened TValues type that now accepts required properties (line 48), there's no compile-time protection against returning a result that's missing required properties.

While this is pre-existing code, consider whether the return type could be tightened to TSearchSchema or a conditional type that reflects which properties might be stripped. This would provide stronger compile-time guarantees aligned with the PR's goal of improving type safety.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router-core/src/searchMiddleware.ts` at line 71, The `as any` cast
on the returned `result` weakens type safety; replace it with a precise type
such as `TSearchSchema` (or a conditional type that reflects stripped/optional
properties) and adjust the middleware function's generics so the compiler can
verify `result` conforms to that type. Concretely, remove `as any` and
cast/annotate `result` to `TSearchSchema` (or a conditional mapping of `TValues`
→ `TSearchSchema`), and tighten the generic constraints around
`TValues`/`TSearchSchema` in the search middleware function so missing required
properties are caught at compile time. Ensure the variable named `result` and
the generics `TValues` and `TSearchSchema` are updated together so the function
signature and return type remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/router-core/src/searchMiddleware.ts`:
- Line 71: The `as any` cast on the returned `result` weakens type safety;
replace it with a precise type such as `TSearchSchema` (or a conditional type
that reflects stripped/optional properties) and adjust the middleware function's
generics so the compiler can verify `result` conforms to that type. Concretely,
remove `as any` and cast/annotate `result` to `TSearchSchema` (or a conditional
mapping of `TValues` → `TSearchSchema`), and tighten the generic constraints
around `TValues`/`TSearchSchema` in the search middleware function so missing
required properties are caught at compile time. Ensure the variable named
`result` and the generics `TValues` and `TSearchSchema` are updated together so
the function signature and return type remain consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0e5affdd-481a-43f7-ac59-1786906de873

📥 Commits

Reviewing files that changed from the base of the PR and between a40df23 and 84e83ce.

📒 Files selected for processing (2)
  • packages/router-core/src/searchMiddleware.ts
  • packages/router-core/tests/searchMiddleware.test-d.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router-core/tests/searchMiddleware.test-d.ts

@schiller-manuel schiller-manuel merged commit 2cca73c into TanStack:main Jun 5, 2026
16 of 18 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 5, 2026
@sprusr

sprusr commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Thank you @schiller-manuel for pushing this over the line

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