diff --git a/.changeset/optional-mutate-parameters.md b/.changeset/optional-mutate-parameters.md new file mode 100644 index 0000000000..0ca970acd9 --- /dev/null +++ b/.changeset/optional-mutate-parameters.md @@ -0,0 +1,12 @@ +--- +'@tanstack/angular-query-experimental': patch +'@tanstack/lit-query': patch +'@tanstack/preact-query': patch +'@tanstack/query-core': patch +'@tanstack/react-query': patch +'@tanstack/solid-query': patch +'@tanstack/svelte-query': patch +'@tanstack/vue-query': patch +--- + +fix: make mutation variables optional when `undefined extends TVariables` diff --git a/packages/angular-query-experimental/src/inject-mutation.ts b/packages/angular-query-experimental/src/inject-mutation.ts index 7eb605047f..8acad49532 100644 --- a/packages/angular-query-experimental/src/inject-mutation.ts +++ b/packages/angular-query-experimental/src/inject-mutation.ts @@ -86,8 +86,12 @@ export function injectMutation< CreateMutateFunction >(() => { const observer = observerSignal() - return (variables, mutateOptions) => { - observer.mutate(variables, mutateOptions).catch(noop) + return ( + ...args: Parameters< + CreateMutateFunction + > + ) => { + observer.mutate(args[0] as TVariables, args[1]).catch(noop) } }) diff --git a/packages/lit-query/src/createMutationController.ts b/packages/lit-query/src/createMutationController.ts index d3133982dd..3dc5ca345e 100644 --- a/packages/lit-query/src/createMutationController.ts +++ b/packages/lit-query/src/createMutationController.ts @@ -1,7 +1,7 @@ import { MutationObserver, type DefaultError, - type MutateOptions, + type MutateFunction, type MutationObserverOptions, type MutationObserverResult, } from '@tanstack/query-core' @@ -45,8 +45,9 @@ export type MutationResultAccessor = * Throws synchronously if no `QueryClient` can be resolved. */ mutate: ( - variables: TVariables, - options?: MutateOptions, + ...args: Parameters< + MutateFunction + > ) => void /** * Starts the mutation and returns the observer promise. @@ -190,14 +191,15 @@ class MutationController< } mutate = ( - variables: TVariables, - mutateOptions?: MutateOptions, + ...args: Parameters< + MutateFunction + > ): void => { if (!this.syncClient() || !this.observer) { throw createMissingQueryClientError() } - void this.observer.mutate(variables, mutateOptions).catch(() => { + void this.observer.mutate(args[0] as TVariables, args[1]).catch(() => { // Intentionally swallow in sync mutate path. }) } @@ -212,7 +214,7 @@ class MutationController< return Promise.reject(createMissingQueryClientError()) } - return this.observer.mutate(...args) + return this.observer.mutate(args[0] as TVariables, args[1]) } reset: MutationObserverResult< diff --git a/packages/preact-query/src/useMutation.ts b/packages/preact-query/src/useMutation.ts index 4612aacc26..495aaadd91 100644 --- a/packages/preact-query/src/useMutation.ts +++ b/packages/preact-query/src/useMutation.ts @@ -52,8 +52,12 @@ export function useMutation< const mutate = useCallback< UseMutateFunction >( - (variables, mutateOptions) => { - observer.mutate(variables, mutateOptions).catch(noop) + ( + ...args: Parameters< + UseMutateFunction + > + ) => { + observer.mutate(args[0] as TVariables, args[1]).catch(noop) }, [observer], ) diff --git a/packages/query-core/src/__tests__/mutations.test-d.tsx b/packages/query-core/src/__tests__/mutations.test-d.tsx new file mode 100644 index 0000000000..53465d601c --- /dev/null +++ b/packages/query-core/src/__tests__/mutations.test-d.tsx @@ -0,0 +1,229 @@ +import { describe, expectTypeOf, it } from 'vitest' + +import type { DefaultError, MutateFunction, MutateOptions } from '../types' + +describe('MutateFunction', () => { + it('void variables', () => { + const mutate = {} as MutateFunction + + expectTypeOf[0]>().toEqualTypeOf< + undefined | void + >() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + mutate() // can be called with no arguments + mutate(undefined, { + onError: (e) => { + expectTypeOf(e).toEqualTypeOf() + }, + }) + }) + + it('optional undefinable variables', () => { + const mutate = {} as MutateFunction< + unknown, + DefaultError, + number | undefined, + unknown + > + + expectTypeOf[0]>().toEqualTypeOf< + number | undefined + >() + + expectTypeOf[1]>().toEqualTypeOf< + | undefined + | MutateOptions + >() + + mutate() // can be called with no arguments + mutate(undefined, { + onError: (e) => { + expectTypeOf(e).toEqualTypeOf() + }, + }) + }) + + it('unknown variables', () => { + const mutate = {} as MutateFunction + + expectTypeOf[0]>().toEqualTypeOf() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + mutate() // can be called with no arguments + }) + + it('any variables', () => { + const mutate = {} as MutateFunction + + expectTypeOf[0]>().toEqualTypeOf() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + mutate() // can be called with no arguments + }) + + it('void union variables', () => { + const mutate = {} as MutateFunction< + unknown, + DefaultError, + void | string, + unknown + > + + expectTypeOf[0]>().toEqualTypeOf< + void | string | undefined + >() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + mutate() // can be called with no arguments + }) + + it('required non-undefinable variables', () => { + const mutate = {} as MutateFunction + + expectTypeOf[0]>().toEqualTypeOf() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + // @ts-expect-error --- required variables + mutate() + mutate(123, { + onError: (e) => { + expectTypeOf(e).toEqualTypeOf() + }, + }) + }) + + describe('compatible with spread arguments pattern', () => { + // this is common pattern used internal so we need make sure it still works + + it('void variables', () => { + const mutate = {} as (...options: Parameters) => void + + expectTypeOf[0]>().toEqualTypeOf< + undefined | void + >() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + mutate() // can be called with no arguments + mutate(undefined, { + onError: (e) => { + expectTypeOf(e).toEqualTypeOf() + }, + }) + }) + + it('optional undefinable variables', () => { + const mutate = {} as ( + ...options: Parameters< + MutateFunction + > + ) => void + + expectTypeOf[0]>().toEqualTypeOf< + number | undefined + >() + + expectTypeOf[1]>().toEqualTypeOf< + | undefined + | MutateOptions + >() + + mutate() // can be called with no arguments + mutate(undefined, { + onError: (e) => { + expectTypeOf(e).toEqualTypeOf() + }, + }) + }) + + it('unknown variables', () => { + const mutate = {} as ( + ...options: Parameters< + MutateFunction + > + ) => void + + expectTypeOf[0]>().toEqualTypeOf() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + mutate() // can be called with no arguments + }) + + it('any variables', () => { + const mutate = {} as ( + ...options: Parameters< + MutateFunction + > + ) => void + + expectTypeOf[0]>().toEqualTypeOf() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + mutate() // can be called with no arguments + }) + + it('void union variables', () => { + const mutate = {} as ( + ...options: Parameters< + MutateFunction + > + ) => void + + expectTypeOf[0]>().toEqualTypeOf< + void | string | undefined + >() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + mutate() // can be called with no arguments + }) + + it('required non-undefinable variables', () => { + const mutate = {} as ( + ...options: Parameters< + MutateFunction + > + ) => void + + expectTypeOf[0]>().toEqualTypeOf() + + expectTypeOf[1]>().toEqualTypeOf< + undefined | MutateOptions + >() + + // @ts-expect-error --- required variables + mutate() + mutate(123, { + onError: (e) => { + expectTypeOf(e).toEqualTypeOf() + }, + }) + }) + }) +}) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index e0220c5978..e8e7111c26 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -1176,14 +1176,28 @@ export interface MutateOptions< ) => void } +export type MutateFunctionRest< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +> = undefined extends TVariables + ? [ + variables?: TVariables, + options?: MutateOptions, + ] + : [ + variables: TVariables, + options?: MutateOptions, + ] + export type MutateFunction< TData = unknown, TError = DefaultError, TVariables = void, TOnMutateResult = unknown, > = ( - variables: TVariables, - options?: MutateOptions, + ...rest: MutateFunctionRest ) => Promise export interface MutationObserverBaseResult< diff --git a/packages/react-query/src/useMutation.ts b/packages/react-query/src/useMutation.ts index 2c66eb8ba8..240c6f70d8 100644 --- a/packages/react-query/src/useMutation.ts +++ b/packages/react-query/src/useMutation.ts @@ -52,8 +52,12 @@ export function useMutation< const mutate = React.useCallback< UseMutateFunction >( - (variables, mutateOptions) => { - observer.mutate(variables, mutateOptions).catch(noop) + ( + ...args: Parameters< + UseMutateFunction + > + ) => { + observer.mutate(args[0] as TVariables, args[1]).catch(noop) }, [observer], ) diff --git a/packages/solid-query/src/useMutation.ts b/packages/solid-query/src/useMutation.ts index 2a2a859652..2d19678baf 100644 --- a/packages/solid-query/src/useMutation.ts +++ b/packages/solid-query/src/useMutation.ts @@ -36,8 +36,8 @@ export function useMutation< TError, TVariables, TOnMutateResult - > = (variables, mutateOptions) => { - observer.mutate(variables, mutateOptions).catch(noop) + > = (...args) => { + observer.mutate(args[0] as TVariables, args[1]).catch(noop) } const [state, setState] = createStore< diff --git a/packages/svelte-query/src/createMutation.svelte.ts b/packages/svelte-query/src/createMutation.svelte.ts index 51ff74827a..9bff1c5614 100644 --- a/packages/svelte-query/src/createMutation.svelte.ts +++ b/packages/svelte-query/src/createMutation.svelte.ts @@ -46,12 +46,11 @@ export function createMutation< observer.setOptions(options()) }) - const mutate = >(( - variables, - mutateOptions, + const mutate: CreateMutateFunction = ( + ...args ) => { - observer.mutate(variables, mutateOptions).catch(noop) - }) + observer.mutate(args[0] as TVariables, args[1]).catch(noop) + } let result = $state(observer.getCurrentResult()) watchChanges( diff --git a/packages/vue-query/src/useMutation.ts b/packages/vue-query/src/useMutation.ts index 9c49730a79..8fbdeecf2f 100644 --- a/packages/vue-query/src/useMutation.ts +++ b/packages/vue-query/src/useMutation.ts @@ -17,7 +17,6 @@ import type { DefaultError, DistributiveOmit, MutateFunction, - MutateOptions, MutationObserverResult, } from '@tanstack/query-core' import type { MaybeRefDeep, MutationOptions } from './types' @@ -108,10 +107,11 @@ export function useMutation< }) const mutate = ( - variables: TVariables, - mutateOptions?: MutateOptions, + ...args: Parameters< + MutateFunction + > ) => { - observer.mutate(variables, mutateOptions).catch(() => { + observer.mutate(args[0] as TVariables, args[1]).catch(() => { // This is intentional }) }