Skip to content

Commit 49211a6

Browse files
committed
fix(types): propagate generic type params to useMutationState select 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 #9825
1 parent 67b12ae commit 49211a6

11 files changed

Lines changed: 202 additions & 42 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@tanstack/react-query': patch
3+
'@tanstack/preact-query': patch
4+
'@tanstack/solid-query': patch
5+
'@tanstack/vue-query': patch
6+
'@tanstack/svelte-query': patch
7+
---
8+
9+
fix(types): propagate generic type parameters to `useMutationState` select callback

packages/preact-query/src/__tests__/useMutationState.test-d.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { MutationState, MutationStatus } from '@tanstack/query-core'
1+
import type { Mutation, MutationState, MutationStatus } from '@tanstack/query-core'
22
import { describe, expectTypeOf, it } from 'vitest'
33

44
import { useMutationState } from '../useMutationState'
@@ -21,4 +21,19 @@ describe('useMutationState', () => {
2121

2222
expectTypeOf(result).toEqualTypeOf<Array<MutationStatus>>()
2323
})
24+
it('should propagate generics to select callback when TResult is typed MutationState', () => {
25+
type MyData = { data: Array<string> }
26+
type MyError = { code: number; message: string }
27+
type MyVars = { id: number }
28+
29+
useMutationState<MutationState<MyData, MyError, MyVars>>({
30+
filters: { mutationKey: ['key'] },
31+
select: (mutation) => {
32+
expectTypeOf(mutation).toEqualTypeOf<
33+
Mutation<MyData, MyError, MyVars, unknown>
34+
>()
35+
return mutation.state
36+
},
37+
})
38+
})
2439
})

packages/preact-query/src/useMutationState.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,44 @@ export function useIsMutating(
2222
).length
2323
}
2424

25-
type MutationStateOptions<TResult = MutationState> = {
25+
type MutationTypeFromResult<TResult> = [TResult] extends [
26+
MutationState<infer TData, infer TError, infer TVariables, infer TOnMutateResult>
27+
]
28+
? Mutation<TData, TError, TVariables, TOnMutateResult>
29+
: Mutation
30+
31+
type MutationStateOptions<
32+
TResult = MutationState,
33+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
34+
> = {
2635
filters?: MutationFilters
27-
select?: (mutation: Mutation) => TResult
36+
select?: (mutation: TMutation) => TResult
2837
}
2938

30-
function getResult<TResult = MutationState>(
39+
function getResult<
40+
TResult = MutationState,
41+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
42+
>(
3143
mutationCache: MutationCache,
32-
options: MutationStateOptions<TResult>,
44+
options: MutationStateOptions<TResult, TMutation>,
3345
): Array<TResult> {
3446
return mutationCache
3547
.findAll(options.filters)
3648
.map(
3749
(mutation): TResult =>
38-
(options.select ? options.select(mutation) : mutation.state) as TResult,
50+
(options.select
51+
? (options.select as unknown as (mutation: Mutation) => TResult)(
52+
mutation,
53+
)
54+
: mutation.state) as TResult,
3955
)
4056
}
4157

42-
export function useMutationState<TResult = MutationState>(
43-
options: MutationStateOptions<TResult> = {},
58+
export function useMutationState<
59+
TResult = MutationState,
60+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
61+
>(
62+
options: MutationStateOptions<TResult, TMutation> = {},
4463
queryClient?: QueryClient,
4564
): Array<TResult> {
4665
const mutationCache = useQueryClient(queryClient).getMutationCache()

packages/react-query/src/__tests__/useMutationState.test-d.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expectTypeOf, it } from 'vitest'
22
import { useMutationState } from '../useMutationState'
3-
import type { MutationState, MutationStatus } from '@tanstack/query-core'
3+
import type { Mutation, MutationState, MutationStatus } from '@tanstack/query-core'
44

55
describe('useMutationState', () => {
66
it('should default to QueryState', () => {
@@ -20,4 +20,19 @@ describe('useMutationState', () => {
2020

2121
expectTypeOf(result).toEqualTypeOf<Array<MutationStatus>>()
2222
})
23+
it('should propagate generics to select callback when TResult is typed MutationState', () => {
24+
type MyData = { data: Array<string> }
25+
type MyError = { code: number; message: string }
26+
type MyVars = { id: number }
27+
28+
useMutationState<MutationState<MyData, MyError, MyVars>>({
29+
filters: { mutationKey: ['key'] },
30+
select: (mutation) => {
31+
expectTypeOf(mutation).toEqualTypeOf<
32+
Mutation<MyData, MyError, MyVars, unknown>
33+
>()
34+
return mutation.state
35+
},
36+
})
37+
})
2338
})

packages/react-query/src/useMutationState.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,25 +22,44 @@ export function useIsMutating(
2222
).length
2323
}
2424

25-
type MutationStateOptions<TResult = MutationState> = {
25+
type MutationTypeFromResult<TResult> = [TResult] extends [
26+
MutationState<infer TData, infer TError, infer TVariables, infer TOnMutateResult>
27+
]
28+
? Mutation<TData, TError, TVariables, TOnMutateResult>
29+
: Mutation
30+
31+
type MutationStateOptions<
32+
TResult = MutationState,
33+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
34+
> = {
2635
filters?: MutationFilters
27-
select?: (mutation: Mutation) => TResult
36+
select?: (mutation: TMutation) => TResult
2837
}
2938

30-
function getResult<TResult = MutationState>(
39+
function getResult<
40+
TResult = MutationState,
41+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
42+
>(
3143
mutationCache: MutationCache,
32-
options: MutationStateOptions<TResult>,
44+
options: MutationStateOptions<TResult, TMutation>,
3345
): Array<TResult> {
3446
return mutationCache
3547
.findAll(options.filters)
3648
.map(
3749
(mutation): TResult =>
38-
(options.select ? options.select(mutation) : mutation.state) as TResult,
50+
(options.select
51+
? (options.select as unknown as (mutation: Mutation) => TResult)(
52+
mutation,
53+
)
54+
: mutation.state) as TResult,
3955
)
4056
}
4157

42-
export function useMutationState<TResult = MutationState>(
43-
options: MutationStateOptions<TResult> = {},
58+
export function useMutationState<
59+
TResult = MutationState,
60+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
61+
>(
62+
options: MutationStateOptions<TResult, TMutation> = {},
4463
queryClient?: QueryClient,
4564
): Array<TResult> {
4665
const mutationCache = useQueryClient(queryClient).getMutationCache()

packages/solid-query/src/__tests__/useMutationState.test-d.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expectTypeOf, it } from 'vitest'
22
import { useMutationState } from '../useMutationState'
3-
import type { MutationState, MutationStatus } from '@tanstack/query-core'
3+
import type { Mutation, MutationState, MutationStatus } from '@tanstack/query-core'
44

55
describe('useMutationState', () => {
66
it('should default to QueryState', () => {
@@ -18,4 +18,25 @@ describe('useMutationState', () => {
1818

1919
expectTypeOf(result()).toEqualTypeOf<Array<MutationStatus>>()
2020
})
21+
it('should propagate generics to select callback when TResult is typed MutationState', () => {
22+
type MyData = { data: Array<string> }
23+
type MyError = { code: number; message: string }
24+
type MyVars = { id: number }
25+
26+
const result = useMutationState<MutationState<MyData, MyError, MyVars>>(
27+
() => ({
28+
filters: { mutationKey: ['key'] },
29+
select: (mutation) => {
30+
expectTypeOf(mutation).toEqualTypeOf<
31+
Mutation<MyData, MyError, MyVars, unknown>
32+
>()
33+
return mutation.state
34+
},
35+
}),
36+
)
37+
38+
expectTypeOf(result()).toEqualTypeOf<
39+
Array<MutationState<MyData, MyError, MyVars, unknown>>
40+
>()
41+
})
2142
})

packages/solid-query/src/useMutationState.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,44 @@ import type {
1010
import type { Accessor } from 'solid-js'
1111
import type { QueryClient } from './QueryClient'
1212

13-
type MutationStateOptions<TResult = MutationState> = {
13+
type MutationTypeFromResult<TResult> = [TResult] extends [
14+
MutationState<infer TData, infer TError, infer TVariables, infer TOnMutateResult>
15+
]
16+
? Mutation<TData, TError, TVariables, TOnMutateResult>
17+
: Mutation
18+
19+
type MutationStateOptions<
20+
TResult = MutationState,
21+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
22+
> = {
1423
filters?: MutationFilters
15-
select?: (mutation: Mutation) => TResult
24+
select?: (mutation: TMutation) => TResult
1625
}
1726

18-
function getResult<TResult = MutationState>(
27+
function getResult<
28+
TResult = MutationState,
29+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
30+
>(
1931
mutationCache: MutationCache,
20-
options: MutationStateOptions<TResult>,
32+
options: MutationStateOptions<TResult, TMutation>,
2133
): Array<TResult> {
2234
return mutationCache
2335
.findAll(options.filters)
2436
.map(
2537
(mutation): TResult =>
26-
(options.select ? options.select(mutation) : mutation.state) as TResult,
38+
(options.select
39+
? (options.select as unknown as (mutation: Mutation) => TResult)(
40+
mutation,
41+
)
42+
: mutation.state) as TResult,
2743
)
2844
}
2945

30-
export function useMutationState<TResult = MutationState>(
31-
options: Accessor<MutationStateOptions<TResult>> = () => ({}),
46+
export function useMutationState<
47+
TResult = MutationState,
48+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
49+
>(
50+
options: Accessor<MutationStateOptions<TResult, TMutation>> = () => ({}),
3251
queryClient?: Accessor<QueryClient>,
3352
): Accessor<Array<TResult>> {
3453
const client = createMemo(() => useQueryClient(queryClient?.()))

packages/svelte-query/src/types.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,19 @@ export type CreateMutationResult<
136136
TOnMutateResult = unknown,
137137
> = CreateBaseMutationResult<TData, TError, TVariables, TOnMutateResult>
138138

139+
type MutationTypeFromResult<TResult> = [TResult] extends [
140+
MutationState<infer TData, infer TError, infer TVariables, infer TOnMutateResult>
141+
]
142+
? Mutation<TData, TError, TVariables, TOnMutateResult>
143+
: Mutation<unknown, DefaultError, unknown, unknown>
144+
139145
/** Options for useMutationState */
140-
export type MutationStateOptions<TResult = MutationState> = {
146+
export type MutationStateOptions<
147+
TResult = MutationState,
148+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
149+
> = {
141150
filters?: MutationFilters
142-
select?: (
143-
mutation: Mutation<unknown, DefaultError, unknown, unknown>,
144-
) => TResult
151+
select?: (mutation: TMutation) => TResult
145152
}
146153

147154
export type QueryClientProviderProps = {

packages/svelte-query/src/useMutationState.svelte.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
11
import { replaceEqualDeep } from '@tanstack/query-core'
22
import { useQueryClient } from './useQueryClient.js'
33
import type {
4+
Mutation,
45
MutationCache,
56
MutationState,
67
QueryClient,
78
} from '@tanstack/query-core'
89
import type { MutationStateOptions } from './types.js'
910

10-
function getResult<TResult = MutationState>(
11+
type MutationTypeFromResult<TResult> = [TResult] extends [
12+
MutationState<infer TData, infer TError, infer TVariables, infer TOnMutateResult>
13+
]
14+
? Mutation<TData, TError, TVariables, TOnMutateResult>
15+
: Mutation
16+
17+
function getResult<
18+
TResult = MutationState,
19+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
20+
>(
1121
mutationCache: MutationCache,
12-
options: MutationStateOptions<TResult>,
22+
options: MutationStateOptions<TResult, TMutation>,
1323
): Array<TResult> {
1424
return mutationCache
1525
.findAll(options.filters)
1626
.map(
1727
(mutation): TResult =>
18-
(options.select ? options.select(mutation) : mutation.state) as TResult,
28+
(options.select
29+
? (options.select as unknown as (mutation: Mutation) => TResult)(
30+
mutation,
31+
)
32+
: mutation.state) as TResult,
1933
)
2034
}
2135

22-
export function useMutationState<TResult = MutationState>(
23-
options: MutationStateOptions<TResult> = {},
36+
export function useMutationState<
37+
TResult = MutationState,
38+
TMutation extends Mutation<any, any, any, any> = MutationTypeFromResult<TResult>,
39+
>(
40+
options: MutationStateOptions<TResult, TMutation> = {},
2441
queryClient?: QueryClient,
2542
): Array<TResult> {
2643
const mutationCache = useQueryClient(queryClient).getMutationCache()

packages/svelte-query/tests/useMutationState/SelectExample.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
mutationStateOpts,
1717
}: {
1818
mutationOpts: Accessor<CreateMutationOptions>
19-
mutationStateOpts: MutationStateOptions<any>
19+
mutationStateOpts: MutationStateOptions<any, any>
2020
} = $props()
2121
2222
const queryClient = new QueryClient()

0 commit comments

Comments
 (0)