Skip to content

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

Open
Zelys-DFKH wants to merge 6 commits into
TanStack:mainfrom
Zelys-DFKH:fix/use-mutation-state-generics
Open

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

Conversation

@Zelys-DFKH
Copy link
Copy Markdown
Contributor

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

🎯 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
    • Fixed generic type propagation in useMutationState across React, Preact, Solid, Vue, and Svelte packages so the select callback receives correctly typed mutation objects for improved type safety and inference.
  • Tests
    • Added type-level tests that assert generics propagate into the select callback and the hook return types.

…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
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 1, 2026

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

Run ID: 49f11e8d-0b05-4493-931c-f41ba0ee76ae

📥 Commits

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

📒 Files selected for processing (6)
  • packages/preact-query/src/__tests__/useMutationState.test-d.tsx
  • packages/preact-query/src/useMutationState.ts
  • packages/react-query/src/useMutationState.ts
  • packages/solid-query/src/useMutationState.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 (4)
  • packages/preact-query/src/tests/useMutationState.test-d.tsx
  • packages/svelte-query/src/useMutationState.svelte.ts
  • packages/react-query/src/useMutationState.ts
  • packages/preact-query/src/useMutationState.ts

📝 Walkthrough

Walkthrough

This PR propagates generic type parameters into the useMutationState select callback by adding MutationTypeFromResult and a TMutation generic across adapters and tests.

Changes

useMutationState generic propagation

Layer / File(s) Summary
Release Announcement
.changeset/fix-mutation-state-generics.md
Announces patch releases for five TanStack Query packages with a types fix for generic propagation in useMutationState select callback.
Hook Implementations & types
packages/react-query/src/useMutationState.ts, packages/preact-query/src/useMutationState.ts, packages/solid-query/src/useMutationState.ts, packages/vue-query/src/useMutationState.ts, packages/svelte-query/src/useMutationState.svelte.ts, packages/svelte-query/src/types.ts
Introduce MutationTypeFromResult<TResult>, add TMutation generic to MutationStateOptions and useMutationState, change select signature to (mutation: TMutation) => TResult, and cast mutation values when calling select to preserve runtime behavior.
Type Test Cases
packages/preact-query/src/__tests__/useMutationState.test-d.tsx, (and parallel tests in react/solid adapters)
Add type-level tests asserting select receives a Mutation<...> narrowed from the provided MutationState<...> TResult generic.
Example Component
packages/svelte-query/tests/useMutationState/SelectExample.svelte
Update mutationStateOpts prop type to MutationStateOptions<any, any> to match new generic arity.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #10792: Requests adding JSDoc for the new TMutation parameter; touches the same new generic and signature changes introduced here.

Suggested reviewers

  • TkDodo

Poem

🐰
I munched some types beneath the sun,
Pulled TMutation, one by one.
Select now knows the mutation's shape,
No more casts—no typing scrape! ✨

🚥 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 accurately describes the main change: propagating generic type parameters to the useMutationState select callback, which is the core focus of this PR.
Description check ✅ Passed The description includes all required sections (Changes, Checklist, Release Impact), provides clear motivation with before/after examples, explains the implementation approach, and confirms checklist items are completed.
Linked Issues check ✅ Passed The PR fully addresses issue #9825 by implementing generic type parameter propagation to the useMutationState select callback across all five adapters (react, preact, solid, vue, svelte) using MutationTypeFromResult.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue: adding MutationTypeFromResult conditional types, updating MutationStateOptions with TMutation parameter, and modifying select callback signatures across all adapters with corresponding test files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch fix/use-mutation-state-generics

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.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 1, 2026

View your CI Pipeline Execution ↗ for commit 57bb423

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

☁️ Nx Cloud last updated this comment at 2026-04-01 17:48:35 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 1, 2026

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/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: 57bb423

Copy link
Copy Markdown
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

🧹 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
Copy link
Copy Markdown
Collaborator

TkDodo commented Jun 1, 2026

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
Copy link
Copy Markdown
Collaborator

TkDodo commented Jun 2, 2026

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

Zelys-DFKH and others added 2 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>
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