Skip to content

fix(types): propagate generic type params to useMutationState select callback - #10373

Merged
TkDodo merged 7 commits into
TanStack:mainfrom
Zelys-DFKH:fix/use-mutation-state-generics
Aug 18, 2026
Merged

fix(types): propagate generic type params to useMutationState select callback#10373
TkDodo merged 7 commits into
TanStack:mainfrom
Zelys-DFKH:fix/use-mutation-state-generics

Conversation

@Zelys-DFKH

@Zelys-DFKH Zelys-DFKH commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Closes #9825.

When you pass a typed MutationState<TData, TError, TVariables> as TResult, the select callback now receives Mutation<TData, TError, TVariables, TContext> instead of the base Mutation type. Before this change you had to cast manually:

// before
useMutationState<MutationState<MyData, MyError, MyVars>>({
  select: (mutation) =>
    (mutation as Mutation<MyData, MyError, MyVars>).state.data,
})

// after
useMutationState<MutationState<MyData, MyError, MyVars>>({
  select: (mutation) => mutation.state.data, // mutation is correctly typed
})

How it works

MutationStateOptions gains a second type param TMutation that defaults to MutationTypeFromResult<TResult>:

type MutationTypeFromResult<TResult> = [TResult] extends [
  MutationState<infer TData, infer TError, infer TVariables, infer TOnMutateResult>
]
  ? Mutation<TData, TError, TVariables, TOnMutateResult>
  : Mutation

The tuple wrapper [TResult] extends [...] makes the conditional non-distributive, which stops TypeScript from producing a union when TResult is still unresolved. The second param keeps backward compatibility: callers that do not provide TResult at all get the same Mutation type they always did.

Applied across all five adapters: react, preact, solid, vue, and svelte.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • Improved TypeScript inference for useMutationState across React, Preact, Solid, Svelte, and Vue integrations.
    • The select callback now receives the correctly typed mutation, including data, error, and variable types.
    • Preserved accurate result typing when custom mutation state generics are provided.

…callback

When TResult is a typed MutationState, the select callback parameter
now receives the correctly typed Mutation instead of the base Mutation type.

Adds a second type param TMutation (defaulting to MutationTypeFromResult<TResult>)
to MutationStateOptions across all five framework adapters. Uses a non-distributive
conditional type to avoid union expansion when TResult is unresolved.

Fixes TanStack#9825
@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 70a1c2c6-3685-40e2-a08a-9dc89e72f4b4

📥 Commits

Reviewing files that changed from the base of the PR and between 5bb089d and 7d161c9.

📒 Files selected for processing (10)
  • .changeset/fix-mutation-state-generics.md
  • packages/preact-query/src/__tests__/useMutationState.test-d.tsx
  • packages/preact-query/src/useMutationState.ts
  • packages/react-query/src/__tests__/useMutationState.test-d.tsx
  • packages/react-query/src/useMutationState.ts
  • packages/solid-query/src/__tests__/useMutationState.test-d.tsx
  • packages/solid-query/src/useMutationState.ts
  • packages/svelte-query/src/types.ts
  • packages/svelte-query/src/useMutationState.svelte.ts
  • packages/vue-query/src/useMutationState.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • .changeset/fix-mutation-state-generics.md
  • packages/preact-query/src/tests/useMutationState.test-d.tsx
  • packages/react-query/src/tests/useMutationState.test-d.tsx
  • packages/react-query/src/useMutationState.ts
  • packages/preact-query/src/useMutationState.ts
  • packages/vue-query/src/useMutationState.ts
  • packages/solid-query/src/tests/useMutationState.test-d.tsx
  • packages/svelte-query/src/types.ts
  • packages/svelte-query/src/useMutationState.svelte.ts
  • packages/solid-query/src/useMutationState.ts

Included review availability: Your plan includes up to 10 reviews per rolling hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

useMutationState now derives mutation generics from MutationState results. Its select callback receives the corresponding typed Mutation across React, Preact, Solid, Svelte, and Vue adapters. Type-level tests verify callback and return types. A changeset records patch releases.

Changes

useMutationState generic propagation

Layer / File(s) Summary
Hook API and implementations
packages/*-query/src/useMutationState*, packages/svelte-query/src/types.ts, .changeset/fix-mutation-state-generics.md
The adapters derive TMutation from TResult, pass it through MutationStateOptions and useMutationState, and use it for select. The changeset records patch releases.
Type tests and assertions
packages/{react,preact,solid}-query/src/__tests__/useMutationState.test-d.tsx
Type-level tests verify that explicit MutationState generics reach the select callback and the returned mutation state array.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 7d161

This localized type-inference change improves the select callback types across the supported adapters without introducing an actionable merge-blocking risk; it is merge-ready after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary type inference fix for the useMutationState select callback.
Description check ✅ Passed The description includes the required changes, checklist, release impact, motivation, implementation details, and changeset information.
Linked Issues check ✅ Passed The PR addresses issue #9825 by propagating MutationState generic parameters to the useMutationState select callback and adding type-level tests.
Out of Scope Changes check ✅ Passed The changes remain focused on useMutationState type inference across the five supported adapters and related release metadata.
✨ 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.

@nx-cloud

nx-cloud Bot commented Apr 1, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 7d161c9

Command Status Duration Result
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 1m 4s View ↗
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 3m 48s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-18 07:59:30 UTC

@pkg-pr-new

pkg-pr-new Bot commented Apr 1, 2026

Copy link
Copy Markdown
More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@10373

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@10373

@tanstack/lit-query

npm i https://pkg.pr.new/@tanstack/lit-query@10373

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@10373

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@10373

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@10373

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@10373

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@10373

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@10373

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@10373

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@10373

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@10373

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@10373

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@10373

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@10373

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@10373

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@10373

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@10373

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@10373

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@10373

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@10373

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@10373

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@10373

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@10373

commit: 7d161c9

@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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/svelte-query/src/useMutationState.svelte.ts (1)

34-38: Simplify callback cast by casting the mutation value instead.

The current double-cast at line 35 obscures that options.select expects TMutation (per the type definition). Casting mutation as TMutation at the call site is clearer and type-safe.

Suggested refactor
-        (options.select
-          ? (options.select as unknown as (mutation: Mutation) => TResult)(
-              mutation,
-            )
-          : mutation.state) as TResult,
+        (options.select
+          ? options.select(mutation as TMutation)
+          : mutation.state) as TResult,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/svelte-query/src/useMutationState.svelte.ts` around lines 34 - 38,
The double-cast of options.select is confusing; instead cast the mutation value
to the expected TMutation and call the select callback directly. Update the
conditional expression in useMutationState (where options.select is invoked) to
call options.select(mutation as TMutation) and keep the fallback to
mutation.state, ensuring the overall expression is still cast to TResult;
reference the symbols options.select, mutation, TMutation, TResult, and
mutation.state when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/preact-query/src/__tests__/useMutationState.test-d.tsx`:
- Around line 1-8: ESLint import/order wants the local module import before
external package type imports; reorder the imports so the relative import of
useMutationState comes before the type-only import from `@tanstack/query-core`
(i.e., move "import { useMutationState } from '../useMutationState'" above the
"import type { Mutation, MutationState, MutationStatus } from
'@tanstack/query-core'").

---

Nitpick comments:
In `@packages/svelte-query/src/useMutationState.svelte.ts`:
- Around line 34-38: The double-cast of options.select is confusing; instead
cast the mutation value to the expected TMutation and call the select callback
directly. Update the conditional expression in useMutationState (where
options.select is invoked) to call options.select(mutation as TMutation) and
keep the fallback to mutation.state, ensuring the overall expression is still
cast to TResult; reference the symbols options.select, mutation, TMutation,
TResult, and mutation.state when making the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b193f4a2-1fbb-40ac-be61-462d3ccab642

📥 Commits

Reviewing files that changed from the base of the PR and between 67b12ae and b73c36a.

📒 Files selected for processing (11)
  • .changeset/fix-mutation-state-generics.md
  • packages/preact-query/src/__tests__/useMutationState.test-d.tsx
  • packages/preact-query/src/useMutationState.ts
  • packages/react-query/src/__tests__/useMutationState.test-d.tsx
  • packages/react-query/src/useMutationState.ts
  • packages/solid-query/src/__tests__/useMutationState.test-d.tsx
  • packages/solid-query/src/useMutationState.ts
  • packages/svelte-query/src/types.ts
  • packages/svelte-query/src/useMutationState.svelte.ts
  • packages/svelte-query/tests/useMutationState/SelectExample.svelte
  • packages/vue-query/src/useMutationState.ts

Comment thread packages/preact-query/src/__tests__/useMutationState.test-d.tsx Outdated
Replace the double-cast `(fn as unknown as (m: Mutation) => TResult)(m)`
with the direct `fn(m as TMutation)` across all five framework packages.

The constraint `TMutation extends Mutation<any, any, any, any>` creates
sufficient overlap for the single cast to compile cleanly on TS 5.4–6.0.
@Zelys-DFKH

Copy link
Copy Markdown
Contributor Author

Hey team, appreciate you all. This has been sitting for a few weeks, and wanted to check in: is there anything blocking review, or should I prioritize other contributions?

This one's pretty straightforward—just propagating generic params through useMutationState's select callback so users don't have to cast manually (issue #9825). Tested across the test suite, no regressions. Happy to adjust anything or provide more context if helpful.

No pressure either way—just want to make sure it's on the radar.

@TkDodo

TkDodo commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

thanks, sorry it took so long. It looks pretty similar to #10790 but that one is a bit simpler on the types?

…order rule

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Zelys-DFKH

Copy link
Copy Markdown
Contributor Author

Hey @TkDodo — took a look at both.

The ergonomic difference comes down to which entry point users reach for first. With #10790:

// needs annotation on every call
useMutationState({ select: (m: Mutation<User, Error, Vars>) => m.state })

With this PR:

// one generic, no annotation needed
useMutationState<MutationState<User, Error, Vars>>({ select: (m) => m.state })

Both work. The <MutationState<...>>() pattern lands more naturally because it mirrors how useMutation is typed — and that's what the issue thread shows people expecting.

I also found a gap neither PR fixes: passing mutationOptions() as filters silently drops all type info. The existing test in mutationOptions.test-d.tsx documents this as expected-but-broken:

useMutationState({ filters: mutationOptions({ mutationKey: ['key'], mutationFn: () => Promise.resolve(5) }) })
// currently: Array<MutationState<unknown, Error, unknown, unknown>>
// should be: Array<MutationState<number, Error, void, unknown>>

Proper fix needs a phantom type brand on mutationOptions() return — worth a follow-up PR, or out of scope?

If you want #10790's approach, I can drop MutationTypeFromResult and go explicit TMutation.

@TkDodo

TkDodo commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

alright, let’s go with your approach. can you fix the conflicts please

Zelys-DFKH and others added 3 commits June 2, 2026 13:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…finition

Export MutationTypeFromResult from types.ts so useMutationState.svelte.ts
can import it instead of redefining it. Also normalises the else branch to
plain Mutation, matching every other adapter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@TkDodo
TkDodo merged commit 6e3d521 into TanStack:main Aug 18, 2026
9 checks passed
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.

useMutationState does not propagate generics into select callback (type inference lost)

2 participants