useMutationState does not propagate generics into select callback (type inference lost)
Describe the bug
The useMutationState hook doesn’t propagate generic type parameters (TData, TError, TVariables, TContext) into the select callback. As a result, when filtering by a specific mutationKey, TypeScript doesn’t infer correct types for mutation.state, forcing developers to cast manually.
Your minimal, reproducible example
https://www.typescriptlang.org/play/?#code/FAFwngDgpgBAsgVxAQxASwPYDsDKKQIDOMAvDAORoAmANlOTAD4XRZVpYDmDz5hCAYwFRChHhSgAnSRknkA3MGAcQUgGbJhMAEpROaQqskwA3sAC+S8NBgARKBoQ0QAUWmzSOvQaMwoAD1U2YjMYMJgqB2QnV3dJAC4YDjUpGAAVNxlJRXMYAH50zI9EouylFXVNWER8TFx8KAAeNNtUZE8ELABrLAwAdywAGkK4z3tHZ1LhtIA1ZEk0ZAAjOmIyTp7+ofSAYWxVQI7u3oGAPlNgcJgBfYCQRLS9rAOQJhhOyLUOKCpFK6o2g9Wig3h8HN9fpdwlIsg9Sm8sE4aH9who0DQEJIoHtOvcYIiALZLKQosJojFY3TIQjYOGjZiImjIqFhAwABWihB+iSWGAwdGQWFJMEMqCIiRqqDqeDFhGFADd5osViIHnMFstVqC2OCsD9hfwlgS0CBVFQAIJ4wnEsqWUCQapIKXYADSUDAnipVGwNDA5ukyDAjQ2Jywp0UymelS0kvQ2AA8hA41hCM1ge11scttN4WRxtFJnFpurlVqyPKMNRpk8XkdNmcLlcCU7kwAxLB5RKCsDC5u1V3uzvwFt1N09lkwbCxqBDgAUio1KsIaqVmpEAEpSOc2TJjVzmjW7trPhDzsxHrdDswwV89ZCrtgcIJhKI5wCUEC2sMF6XVekS2uhDDDcUaBA8h6BJuJDbruBhNCGWxnu8WYDMK2ClHOMKyHSWTfquS4rouqzAZeeIXqBrzXjqt4-FBMEYHu8EoWG2r1kKE6PlApp0FQb6Auk6bHrqPzDFhCQjFkCJInhRF-rM+HEdcpHgaRQk0VQdEwDuDFwcGzFIQhqETliICSGAQ7dsKJlmfYNCBhZWDjlceogH0shdHAGCRA5TnhJwAhpGgBIzok1okhOAD6nwFmaQ68vyUCCr2XHID5Bo3NAaUWEokQCHZWIwMshiSJorx5dSxC6ASGCKiqjbhAAxBACyKqowr+YFwWhQgRLhf8IimRgYCzuuiQVtQwrNRgqgCGaIoCAAFj8ThQAA4gII1jZW97hFNM1zQgEDvmtAVBVAs56n062dVA3W9cYVEnneo0wONO1hHtUCzT81wCpI11nRgSCba922TTI+0-UVpmlZOSZ1MgNBVTV50vW9OQ5V9+WwOVojDv2WBpm0dahjmox5lEMRTP+CkiCT2a7KpmZsecdxQMEXjVbVdD1WETUtagUAGg0Eojo+DREygZO4TTslAYzFHhhx8PYMu+POlgibJqmLRfhJsjFrT8vkS8StXFiyDelgvowH2GsAJJUHdNrCiBxWCCAsiziBXycF2jnrgaXFa3UhCzhgKspqLBMh6rkvINLBuy7+xsQSApxo2DE6cFxtspSD3ZqRCwqW1Q8ZLFykjylI4cV1I1fid2mcTcZUBc1A5eVw3tdd1I-tgM370wJ931UHDyaI8j1cg+jE4gegiKo4k2mMXpLPCgEX1IOdP6AYRKcvSvum6ygSt2hUkgaDGYtYK26JGCEE4APQAFQvxOMAv-AqCLbbN8wF0d0fh-ClV9B-F+T8JwBFKnFPkAp2JXFfu-K4n8YD2ywHlBAkQ-4E2IASH+C0OCcBgCAQhxBmo-DQAIQWMA1CdFmnUcBkCrgUPYNQ1Qc47bJmjhrRo3Zhj8MKo5DOW4YDxXgcKJB4C0EYIxNgrhodbYEKISQshOCNYAPdEwicCiBzmR4cmMcki37SLvs4VISwPS6KwCKfARBtFXFFAQQgQ5YzSjsXKbK9obBuPFoLWOKZmi6H4M4Twvj6iC3OGQUIqJ75SBcQYuoZiH5BzoLNThN9EnYE0mkYJMQcgRlytjX6FUYAAEUEBSDADsGgaB2avDMHaIp8xYB0Iwcmd4XJwkylUEEkQMQwk3x6VAU44dI4JPVsmYZASdZ5OcKcYYABHSpZkal1OeEOCpVS1n1Jev6EqQZcn9PmRGN2rw1B8g6F0oZEsJzdIliYCI-FipEIANoAF0YDmGGI8m4kRnYkjzqIZAOdEgvK4F8n5SQnb4h6jaL5pxgCjMeV8cxkg1aPOsWORIrzyAuhcAATXIJ875IooBpLxLOR5TjYDmE0jSr564P4oJZaytlbKn5PxgAAPTyEAA
Steps to reproduce
import { useMutationState } from '@tanstack/react-query'
type MyData = { data: string[] }
type MyError = { code: number; message: string }
type MyVars = { id: number }
const result = useMutationState<
MutationState<MyData, MyError, MyVars>
>({
filters: { mutationKey: ['KEY'] },
select: ({ state }) => state,
})
// Type '({ state }: Mutation<unknown, Error, unknown, unknown>) => MutationState<unknown, Error, unknown, unknown>' is not assignable to type '(mutation: Mutation<unknown, Error, unknown, unknown>) => MutationState<{ data: string[]; }, { code: number; message: string; }, { id: number; }, unknown>'.
Expected behavior
When using filters.mutationKey to target a known mutation, the generic arguments should be propagated to the select callback:
select: (mutation: Mutation<MyData, MyError, MyVars>) => mutation.state
Current workaround - explicitly casting inside select:
select: (mutation) =>
mutation.state as MutationState<MyData, MyError, MyVars>
How often does this bug happen?
None
Screenshots or Videos
No response
Platform
All
Tanstack Query adapter
react-query
TanStack Query version
v5.59.15
TypeScript version
v5.9.3
Additional context
No response
filters.mutationKey allows for fuzzy matching, so by passing in ['KEY'], you could potentially get multiple, matching queries at runtime.
Also the type parameter we pass to UseMutationState is for the Result type, not the MutationState itself. we would have to add more type parameters to useMutationState, and since Result is the first type param, it means you would have to always define it, which isn’t nice.
What are you trying to achieve with useMutationState exactly?
best I could come up with is this, but still needs a type assertion:
https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgVwM4FMCyyYEMbAQB2AynjOgDRzbmFFwC+cAZlBCHAOQACeRqPAGMA1gHoo6XEJgBaAI7J0UAJ5cAUOpgqw6GioAi+XHAC8iOABNjALjiCowIgHMA2gF0mWnXswqAolDsUGZwgcHeuvoAarhQqKFIwJZ2RMggAEbKXupCxIJwkqjIADbw5mhYOPj0ZPjoABQI6nCswGXKqHZIINUExADS6Cp2rlwD-gCaXJ6MlC326CXoMnYNC60NDSB2tDXEADx+RnjUfuHQZyqx8dTIRCJEEADuRAB8AJRmb3AgAHSCepfXAJAByxFBpRKuAyywOAAU4rgQOgKPEDtpdBAWCgMHt+qRyOg3q4AAzuElcDDLGQzBYfeaMD5AA
I also stumbled into this by following the v5 documentation for optimistic mutations https://tanstack.com/query/v5/docs/framework/react/reference/useMutationState:
import { useMutation, useMutationState } from '@tanstack/react-query'
const mutationKey = ['posts']
// Some mutation that we want to get the state for
const mutation = useMutation({
mutationKey,
mutationFn: (newPost) => {
return axios.post('/posts', newPost)
},
})
const data = useMutationState({
// this mutation key needs to match the mutation key of the given mutation (see above)
filters: { mutationKey },
select: (mutation) => mutation.state.data,
})
In my case the code looks more like this:
const queryKey = 'preferences' as const;
const mutationKey = 'update-preference' as const;
export const useUpdatePreferences = () => {
const updatePreferences = usePatchPreferences();
const client = useQueryClient();
const mutation = useMutation({
mutationFn: updatePreferences,
onSettled: () => client.invalidateQueries({ queryKey: [queryKey] }),
mutationKey: [mutationKey],
});
const variables = useMutationState({
filters: {
mutationKey: [mutationKey],
status: 'pending',
},
select: (m) => m.state.variables as UpdatePreference,
});
return { ...mutation, variables };
};
where I override variables so that the user can change multiple preferences simultaneously.
Altering your example a bit @TkDodo, would it be possible to do something like this:
import { Mutation, MutationState, MutationFilters, QueryClient } from '@tanstack/react-query'
declare function useMutationState<TResult = MutationState, TSelect = Mutation>(options?: MutationStateOptions<TResult, TSelect>, queryClient?: QueryClient): Array<TResult>;
type MutationStateOptions<TResult = MutationState, TSelect = Mutation> = {
filters?: MutationFilters;
select?: (mutation: TSelect) => TResult | undefined;
};
const result = useMutationState<string, Mutation<unknown, Error, string>>({
filters: { mutationKey: ['KEY'] },
select: (m) => m.state.variables,
});
I am unsure of the 'TResult | undefined' - that comes from TData and TVariables being typed with '| undefined' in the MutationState interface, but the result is still NonNullable. Its a small lie, but I think its always been there, though implicit.
Anyways, the type assertion isn't the end of the world, and I think using variables is a pretty neat way of achieving optimistic updates. If you find the time, let me know what you think. Keep up the good work.
Playground: https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgWQK4wIY2BAdgGhXSx1wGVMYBTQtS0gMWABtqoBnQgRVSqgE8Aws2BVc8AL5wAZlAgg4AcgACmXO0wBjANYB6KFQyaYAWgCOvAYoBQ1gCZVNzDAZmpcx0nFTsqdEngUWFQAPAAqAEpU7KiscAC8RPSBlDRwYWRUzI7wif7YeAB8ABQQYAXqAPwAXEkB5KkA8uWk7OFRMayEGVk5hYQWfEIiYjA1cDxDwqLiAJS1AIJQUBj87dGxMIUA3NYw-GBUdRVB1M0VbZEbcXnEJ6ndmdnGCcekha8I1nA-Mixs7HG+UY-z47F2vzgvmeY1qxRAd1ItR6MNmCQ+V068AAPt5cA5pMBcFQ7LsJLtrJo8Bo4AYsa8fH5ESlgiENFAiQBzWjM3AhdzaXAQADuBDgAFFltBCOyuYUSl8foTWGDakgEclcABpKj8WoAbUUWvFAE1FABdOASfDfKG9YxwkBo+IfOAgAB0GmC7oAbi5gBgAEbZTjWCSzXZAA