Skip to content

Commit ec757c0

Browse files
committed
fix(angular-query): align mutation options to most recent react-query types
1 parent 34657e5 commit ec757c0

File tree

5 files changed

+259
-37
lines changed

5 files changed

+259
-37
lines changed
Lines changed: 184 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,199 @@
1-
import { assertType, describe, expectTypeOf, test } from 'vitest'
2-
import { mutationOptions } from '../mutation-options'
1+
import { assertType, describe, expectTypeOf, it } from 'vitest'
2+
import { QueryClient } from '@tanstack/query-core'
3+
import {
4+
injectIsMutating,
5+
injectMutation,
6+
injectMutationState,
7+
mutationOptions,
8+
} from '..'
9+
import type {
10+
DefaultError,
11+
MutationState,
12+
WithRequired,
13+
} from '@tanstack/query-core'
14+
import type { CreateMutationOptions, CreateMutationResult } from '../types'
315

416
describe('mutationOptions', () => {
5-
test('should not allow excess properties', () => {
6-
assertType<Parameters<typeof mutationOptions>>([
7-
{
8-
mutationFn: () => Promise.resolve(5),
9-
mutationKey: ['key'],
10-
// @ts-expect-error this is a good error, because onMutates does not exist!
11-
onMutates: 1000,
17+
it('should not allow excess properties', () => {
18+
// @ts-expect-error this is a good error, because onMutates does not exist!
19+
mutationOptions({
20+
mutationFn: () => Promise.resolve(5),
21+
mutationKey: ['key'],
22+
onMutates: 1000,
23+
onSuccess: (data) => {
24+
expectTypeOf(data).toEqualTypeOf<number>()
1225
},
13-
])
14-
assertType<Parameters<typeof mutationOptions>>([
15-
{
16-
mutationFn: () => Promise.resolve(5),
17-
mutationKey: ['key'],
18-
// @ts-expect-error this is a good error, because onMutates does not exist!
19-
onMutates: 1000,
26+
})
27+
})
28+
29+
it('should infer types for callbacks', () => {
30+
mutationOptions({
31+
mutationFn: () => Promise.resolve(5),
32+
mutationKey: ['key'],
33+
onSuccess: (data) => {
34+
expectTypeOf(data).toEqualTypeOf<number>()
2035
},
21-
])
36+
})
2237
})
2338

24-
test('should infer types for callbacks', () => {
39+
it('should infer types for onError callback', () => {
2540
mutationOptions({
41+
mutationFn: () => {
42+
throw new Error('fail')
43+
},
44+
mutationKey: ['key'],
45+
onError: (error) => {
46+
expectTypeOf(error).toEqualTypeOf<DefaultError>()
47+
},
48+
})
49+
})
50+
51+
it('should infer types for variables', () => {
52+
mutationOptions<number, DefaultError, { id: string }>({
53+
mutationFn: (vars) => {
54+
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
55+
return Promise.resolve(5)
56+
},
57+
mutationKey: ['with-vars'],
58+
})
59+
})
60+
61+
it('should infer context type correctly', () => {
62+
mutationOptions<number, DefaultError, void, { name: string }>({
2663
mutationFn: () => Promise.resolve(5),
2764
mutationKey: ['key'],
65+
onMutate: () => {
66+
return { name: 'context' }
67+
},
68+
onSuccess: (_data, _variables, context) => {
69+
expectTypeOf(context).toEqualTypeOf<{ name: string }>()
70+
},
71+
})
72+
})
73+
74+
it('should error if mutationFn return type mismatches TData', () => {
75+
assertType(
76+
mutationOptions<number>({
77+
// @ts-expect-error this is a good error, because return type is string, not number
78+
mutationFn: async () => Promise.resolve('wrong return'),
79+
}),
80+
)
81+
})
82+
83+
it('should allow mutationKey to be omitted', () => {
84+
return mutationOptions({
85+
mutationFn: () => Promise.resolve(123),
2886
onSuccess: (data) => {
2987
expectTypeOf(data).toEqualTypeOf<number>()
3088
},
3189
})
3290
})
91+
92+
it('should infer all types when not explicitly provided', () => {
93+
expectTypeOf(
94+
mutationOptions({
95+
mutationFn: (id: string) => Promise.resolve(id.length),
96+
mutationKey: ['key'],
97+
onSuccess: (data) => {
98+
expectTypeOf(data).toEqualTypeOf<number>()
99+
},
100+
}),
101+
).toEqualTypeOf<
102+
WithRequired<
103+
CreateMutationOptions<number, DefaultError, string>,
104+
'mutationKey'
105+
>
106+
>()
107+
expectTypeOf(
108+
mutationOptions({
109+
mutationFn: (id: string) => Promise.resolve(id.length),
110+
onSuccess: (data) => {
111+
expectTypeOf(data).toEqualTypeOf<number>()
112+
},
113+
}),
114+
).toEqualTypeOf<
115+
Omit<CreateMutationOptions<number, DefaultError, string>, 'mutationKey'>
116+
>()
117+
})
118+
119+
it('should infer types when used with injectMutation', () => {
120+
const mutation = injectMutation(() =>
121+
mutationOptions({
122+
mutationKey: ['key'],
123+
mutationFn: () => Promise.resolve('data'),
124+
onSuccess: (data) => {
125+
expectTypeOf(data).toEqualTypeOf<string>()
126+
},
127+
}),
128+
)
129+
expectTypeOf(mutation).toEqualTypeOf<
130+
CreateMutationResult<string, DefaultError, void, unknown>
131+
>()
132+
133+
injectMutation(
134+
// should allow when used with injectMutation without mutationKey
135+
() =>
136+
mutationOptions({
137+
mutationFn: () => Promise.resolve('data'),
138+
onSuccess: (data) => {
139+
expectTypeOf(data).toEqualTypeOf<string>()
140+
},
141+
}),
142+
)
143+
})
144+
145+
it('should infer types when used with injectIsMutating', () => {
146+
const isMutating = injectIsMutating(
147+
mutationOptions({
148+
mutationKey: ['key'],
149+
mutationFn: () => Promise.resolve(5),
150+
}),
151+
)
152+
expectTypeOf(isMutating()).toEqualTypeOf<number>()
153+
154+
injectIsMutating(
155+
// @ts-expect-error filters should have mutationKey
156+
mutationOptions({
157+
mutationFn: () => Promise.resolve(5),
158+
}),
159+
)
160+
})
161+
162+
it('should infer types when used with queryClient.isMutating', () => {
163+
const queryClient = new QueryClient()
164+
165+
const isMutating = queryClient.isMutating(
166+
mutationOptions({
167+
mutationKey: ['key'],
168+
mutationFn: () => Promise.resolve(5),
169+
}),
170+
)
171+
expectTypeOf(isMutating).toEqualTypeOf<number>()
172+
173+
queryClient.isMutating(
174+
// @ts-expect-error filters should have mutationKey
175+
mutationOptions({
176+
mutationFn: () => Promise.resolve(5),
177+
}),
178+
)
179+
})
180+
181+
it('should infer types when used with injectMutationState', () => {
182+
const mutationState = injectMutationState(() => ({
183+
filters: mutationOptions({
184+
mutationKey: ['key'],
185+
mutationFn: () => Promise.resolve(5),
186+
}),
187+
}))
188+
expectTypeOf(mutationState()).toEqualTypeOf<
189+
Array<MutationState<unknown, Error, unknown, unknown>>
190+
>()
191+
192+
injectMutationState({
193+
// @ts-expect-error filters should have mutationKey
194+
filters: mutationOptions({
195+
mutationFn: () => Promise.resolve(5),
196+
}),
197+
})
198+
})
33199
})

packages/angular-query-experimental/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type {
1212
} from './query-options'
1313
export { queryOptions } from './query-options'
1414

15-
export type { CreateMutationOptions } from './mutation-options'
15+
export type { CreateMutationOptions } from './types'
1616
export { mutationOptions } from './mutation-options'
1717

1818
export type {

packages/angular-query-experimental/src/inject-mutation.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ import {
1818
} from '@tanstack/query-core'
1919
import { signalProxy } from './signal-proxy'
2020
import type { DefaultError, MutationObserverResult } from '@tanstack/query-core'
21-
import type { CreateMutateFunction, CreateMutationResult } from './types'
22-
import type { CreateMutationOptions } from './mutation-options'
21+
import type {
22+
CreateMutateFunction,
23+
CreateMutationOptions,
24+
CreateMutationResult,
25+
} from './types'
2326

2427
export interface InjectMutationOptions {
2528
/**

packages/angular-query-experimental/src/mutation-options.ts

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import type {
2-
DefaultError,
3-
MutationObserverOptions,
4-
OmitKeyof,
5-
} from '@tanstack/query-core'
1+
import type { DefaultError, WithRequired } from '@tanstack/query-core'
2+
import type { CreateMutationOptions } from './types'
63

74
/**
85
* Allows to share and re-use mutation options in a type-safe way.
@@ -33,28 +30,73 @@ import type {
3330
* ```
3431
* @param options - The mutation options.
3532
* @returns Mutation options.
36-
* @public
3733
*/
3834
export function mutationOptions<
3935
TData = unknown,
4036
TError = DefaultError,
4137
TVariables = void,
4238
TContext = unknown,
4339
>(
44-
options: MutationObserverOptions<TData, TError, TVariables, TContext>,
45-
): CreateMutationOptions<TData, TError, TVariables, TContext> {
46-
return options
47-
}
40+
options: WithRequired<
41+
CreateMutationOptions<TData, TError, TVariables, TContext>,
42+
'mutationKey'
43+
>,
44+
): WithRequired<
45+
CreateMutationOptions<TData, TError, TVariables, TContext>,
46+
'mutationKey'
47+
>
48+
export function mutationOptions<
49+
TData = unknown,
50+
TError = DefaultError,
51+
TVariables = void,
52+
TContext = unknown,
53+
>(
54+
options: Omit<
55+
CreateMutationOptions<TData, TError, TVariables, TContext>,
56+
'mutationKey'
57+
>,
58+
): Omit<
59+
CreateMutationOptions<TData, TError, TVariables, TContext>,
60+
'mutationKey'
61+
>
4862

4963
/**
50-
* @public
64+
* Allows to share and re-use mutation options in a type-safe way.
65+
*
66+
* **Example**
67+
*
68+
* ```ts
69+
* export class QueriesService {
70+
* private http = inject(HttpClient);
71+
*
72+
* updatePost(id: number) {
73+
* return mutationOptions({
74+
* mutationFn: (post: Post) => Promise.resolve(post),
75+
* mutationKey: ["updatePost", id],
76+
* onSuccess: (newPost) => {
77+
* // ^? newPost: Post
78+
* this.queryClient.setQueryData(["posts", id], newPost);
79+
* },
80+
* });
81+
* }
82+
* }
83+
*
84+
* queries = inject(QueriesService)
85+
* idSignal = new Signal(0);
86+
* mutation = injectMutation(() => this.queries.updatePost(this.idSignal()))
87+
*
88+
* mutation.mutate({ title: 'New Title' })
89+
* ```
90+
* @param options - The mutation options.
91+
* @returns Mutation options.
5192
*/
52-
export interface CreateMutationOptions<
93+
export function mutationOptions<
5394
TData = unknown,
5495
TError = DefaultError,
5596
TVariables = void,
5697
TContext = unknown,
57-
> extends OmitKeyof<
58-
MutationObserverOptions<TData, TError, TVariables, TContext>,
59-
'_defaulted'
60-
> {}
98+
>(
99+
options: CreateMutationOptions<TData, TError, TVariables, TContext>,
100+
): CreateMutationOptions<TData, TError, TVariables, TContext> {
101+
return options
102+
}

packages/angular-query-experimental/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
InfiniteQueryObserverOptions,
88
InfiniteQueryObserverResult,
99
MutateFunction,
10+
MutationObserverOptions,
1011
MutationObserverResult,
1112
OmitKeyof,
1213
Override,
@@ -158,6 +159,16 @@ export type DefinedCreateInfiniteQueryResult<
158159
>,
159160
> = MapToSignals<TDefinedInfiniteQueryObserver>
160161

162+
export interface CreateMutationOptions<
163+
TData = unknown,
164+
TError = DefaultError,
165+
TVariables = void,
166+
TContext = unknown,
167+
> extends OmitKeyof<
168+
MutationObserverOptions<TData, TError, TVariables, TContext>,
169+
'_defaulted'
170+
> {}
171+
161172
/**
162173
* @public
163174
*/

0 commit comments

Comments
 (0)