Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/many-symbols-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-query': minor
---

feat(vue-query): add 'mutationOptions'
220 changes: 220 additions & 0 deletions packages/vue-query/src/__tests__/mutationOptions.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { assertType, describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '@tanstack/query-core'
import { useMutation } from '../useMutation'
import { useIsMutating, useMutationState } from '../useMutationState'
import { mutationOptions } from '../mutationOptions'
import type {
DefaultError,
MutationFunctionContext,
MutationState,
WithRequired,
} from '@tanstack/query-core'
import type { Ref } from 'vue-demi'
import type { VueMutationOptions } from '../types'

describe('mutationOptions', () => {
it('should not allow excess properties', () => {
// @ts-expect-error this is a good error, because onMutates does not exist!
mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onMutates: 1000,
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})

it('should infer types for callbacks', () => {
mutationOptions({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})

it('should infer types for onError callback', () => {
mutationOptions({
mutationFn: () => {
throw new Error('fail')
},
mutationKey: ['key'],
onError: (error) => {
expectTypeOf(error).toEqualTypeOf<DefaultError>()
},
})
})

it('should infer types for variables', () => {
mutationOptions<number, DefaultError, { id: string }>({
mutationFn: (vars) => {
expectTypeOf(vars).toEqualTypeOf<{ id: string }>()
return Promise.resolve(5)
},
mutationKey: ['with-vars'],
})
})

it('should infer result type correctly', () => {
mutationOptions<number, DefaultError, void, { name: string }>({
mutationFn: () => Promise.resolve(5),
mutationKey: ['key'],
onMutate: () => {
return { name: 'onMutateResult' }
},
onSuccess: (_data, _variables, onMutateResult) => {
expectTypeOf(onMutateResult).toEqualTypeOf<{ name: string }>()
},
})
})

it('should infer context type correctly', () => {
mutationOptions<number>({
mutationFn: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
return Promise.resolve(5)
},
mutationKey: ['key'],
onMutate: (_variables, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSuccess: (_data, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onError: (_error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
onSettled: (_data, _error, _variables, _onMutateResult, context) => {
expectTypeOf(context).toEqualTypeOf<MutationFunctionContext>()
},
})
})

it('should error if mutationFn return type mismatches TData', () => {
assertType(
mutationOptions<number>({
// @ts-expect-error this is a good error, because return type is string, not number
mutationFn: async () => Promise.resolve('wrong return'),
}),
)
})

it('should allow mutationKey to be omitted', () => {
return mutationOptions({
mutationFn: () => Promise.resolve(123),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
})
})

it('should infer all types when not explicitly provided', () => {
expectTypeOf(
mutationOptions({
mutationFn: (id: string) => Promise.resolve(id.length),
mutationKey: ['key'],
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
}),
).toEqualTypeOf<
WithRequired<
VueMutationOptions<number, DefaultError, string, unknown>,
'mutationKey'
>
>()
expectTypeOf(
mutationOptions({
mutationFn: (id: string) => Promise.resolve(id.length),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<number>()
},
}),
).toEqualTypeOf<
Omit<
VueMutationOptions<number, DefaultError, string, unknown>,
'mutationKey'
>
>()
})

it('should work when used with useMutation', () => {
const mutation = useMutation(
mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}),
)
expectTypeOf(mutation.data.value).toEqualTypeOf<string | undefined>()

// should allow when used with useMutation without mutationKey
useMutation(
mutationOptions({
mutationFn: () => Promise.resolve('data'),
onSuccess: (data) => {
expectTypeOf(data).toEqualTypeOf<string>()
},
}),
)
})

it('should work when used with useIsMutating', () => {
const isMutating = useIsMutating(
mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve(5),
}),
)
expectTypeOf(isMutating).toEqualTypeOf<Ref<number>>()

useIsMutating(
// @ts-expect-error filters should have mutationKey
mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
)
})

it('should work when used with queryClient.isMutating', () => {
const queryClient = new QueryClient()

const isMutating = queryClient.isMutating(
mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve(5),
}),
)
expectTypeOf(isMutating).toEqualTypeOf<number>()

queryClient.isMutating(
// @ts-expect-error filters should have mutationKey
mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
)
})

it('should work when used with useMutationState', () => {
const mutationState = useMutationState({
filters: mutationOptions({
mutationKey: ['key'],
mutationFn: () => Promise.resolve(5),
}),
})
expectTypeOf(mutationState.value).toEqualTypeOf<
Array<MutationState<unknown, Error, unknown, unknown>>
>()

useMutationState({
// @ts-expect-error filters should have mutationKey
filters: mutationOptions({
mutationFn: () => Promise.resolve(5),
}),
})
})
})
Loading
Loading