From 2ddfb1ccb24c5fcabd2e03c72f30240b5320bdc0 Mon Sep 17 00:00:00 2001 From: unnoq Date: Mon, 3 Mar 2025 10:21:25 +0700 Subject: [PATCH 1/9] feat(query-core): make `MutateFunction` optional undefinable-variables --- .../query-core/src/__tests__/types.test-d.tsx | 59 +++++++++++++++++++ packages/query-core/src/types.ts | 18 +++++- 2 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 packages/query-core/src/__tests__/types.test-d.tsx diff --git a/packages/query-core/src/__tests__/types.test-d.tsx b/packages/query-core/src/__tests__/types.test-d.tsx new file mode 100644 index 00000000000..0b0b4cd72de --- /dev/null +++ b/packages/query-core/src/__tests__/types.test-d.tsx @@ -0,0 +1,59 @@ +import { describe, expectTypeOf, it } from 'vitest' +import type { MutateFunction } from 'src/types' + +describe('MutateFunction', () => { + it('optional undefinable variables', () => { + const mutate = {} as MutateFunction< + unknown, + unknown, + number | undefined, + unknown + > + + expectTypeOf[0]>().toEqualTypeOf< + number | undefined + >() + + mutate() // can be called with no arguments + }) + + it('required non-undefinable variables', () => { + const mutate = {} as MutateFunction + + expectTypeOf[0]>().toEqualTypeOf() + + // @ts-expect-error --- required variables + mutate() + }) + + describe('compatible with spread arguments pattern', () => { + // this is common pattern used internal so we need make sure it still works + + it('optional undefinable variables', () => { + const mutate = {} as ( + ...options: Parameters< + MutateFunction + > + ) => void + + expectTypeOf[0]>().toEqualTypeOf< + number | undefined + >() + + mutate() // can be called with no arguments + }) + + it('required non-undefinable variables', () => { + const mutate = {} as ( + ...options: Parameters< + MutateFunction + > + ) => void + + expectTypeOf[0]>().toEqualTypeOf() + + // @ts-expect-error --- required variables + mutate() + }) + }) +}) diff --git a/packages/query-core/src/types.ts b/packages/query-core/src/types.ts index 6d94daabc07..8e1c7bc8c81 100644 --- a/packages/query-core/src/types.ts +++ b/packages/query-core/src/types.ts @@ -1149,14 +1149,28 @@ export interface MutateOptions< ) => void } +export type MutateFunctionRest< + TData = unknown, + TError = DefaultError, + TVariables = void, + TContext = unknown, +> = undefined extends TVariables + ? [ + variables?: TVariables, + options?: MutateOptions, + ] + : [ + variables: TVariables, + options?: MutateOptions, + ] + export type MutateFunction< TData = unknown, TError = DefaultError, TVariables = void, TContext = unknown, > = ( - variables: TVariables, - options?: MutateOptions, + ...rest: MutateFunctionRest ) => Promise export interface MutationObserverBaseResult< From 30858702fb524c8370b3013aed237f62b527fcc8 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 4 Mar 2025 19:46:15 +0700 Subject: [PATCH 2/9] test(query-core): MutateFunction --- .../src/__tests__/mutation.test-d.tsx | 135 ++++++++++++++++++ .../query-core/src/__tests__/types.test-d.tsx | 59 -------- 2 files changed, 135 insertions(+), 59 deletions(-) create mode 100644 packages/query-core/src/__tests__/mutation.test-d.tsx delete mode 100644 packages/query-core/src/__tests__/types.test-d.tsx diff --git a/packages/query-core/src/__tests__/mutation.test-d.tsx b/packages/query-core/src/__tests__/mutation.test-d.tsx new file mode 100644 index 00000000000..f5f15afdfca --- /dev/null +++ b/packages/query-core/src/__tests__/mutation.test-d.tsx @@ -0,0 +1,135 @@ +import { describe, expectTypeOf, it } from 'vitest' +import type { DefaultError, MutateFunction, MutateOptions } from 'src/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('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('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/__tests__/types.test-d.tsx b/packages/query-core/src/__tests__/types.test-d.tsx deleted file mode 100644 index 0b0b4cd72de..00000000000 --- a/packages/query-core/src/__tests__/types.test-d.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expectTypeOf, it } from 'vitest' -import type { MutateFunction } from 'src/types' - -describe('MutateFunction', () => { - it('optional undefinable variables', () => { - const mutate = {} as MutateFunction< - unknown, - unknown, - number | undefined, - unknown - > - - expectTypeOf[0]>().toEqualTypeOf< - number | undefined - >() - - mutate() // can be called with no arguments - }) - - it('required non-undefinable variables', () => { - const mutate = {} as MutateFunction - - expectTypeOf[0]>().toEqualTypeOf() - - // @ts-expect-error --- required variables - mutate() - }) - - describe('compatible with spread arguments pattern', () => { - // this is common pattern used internal so we need make sure it still works - - it('optional undefinable variables', () => { - const mutate = {} as ( - ...options: Parameters< - MutateFunction - > - ) => void - - expectTypeOf[0]>().toEqualTypeOf< - number | undefined - >() - - mutate() // can be called with no arguments - }) - - it('required non-undefinable variables', () => { - const mutate = {} as ( - ...options: Parameters< - MutateFunction - > - ) => void - - expectTypeOf[0]>().toEqualTypeOf() - - // @ts-expect-error --- required variables - mutate() - }) - }) -}) From 02db0f2cfbc00237951c7ad9b7f543d17bdb6101 Mon Sep 17 00:00:00 2001 From: unnoq Date: Tue, 4 Mar 2025 19:52:07 +0700 Subject: [PATCH 3/9] test(query-core): MutateFunction --- .../src/__tests__/{mutation.test-d.tsx => mutations.test-d.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/query-core/src/__tests__/{mutation.test-d.tsx => mutations.test-d.tsx} (100%) diff --git a/packages/query-core/src/__tests__/mutation.test-d.tsx b/packages/query-core/src/__tests__/mutations.test-d.tsx similarity index 100% rename from packages/query-core/src/__tests__/mutation.test-d.tsx rename to packages/query-core/src/__tests__/mutations.test-d.tsx From 9f0282b85b065187f3ca21b5b7f063cca61a8138 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Sat, 22 Aug 2026 09:40:40 +0200 Subject: [PATCH 4/9] vue --- .../src/__tests__/mutations.test-d.tsx | 99 +++++++++++++++++++ packages/vue-query/src/useMutation.ts | 8 +- 2 files changed, 103 insertions(+), 4 deletions(-) diff --git a/packages/query-core/src/__tests__/mutations.test-d.tsx b/packages/query-core/src/__tests__/mutations.test-d.tsx index f5f15afdfca..d8f0d49370a 100644 --- a/packages/query-core/src/__tests__/mutations.test-d.tsx +++ b/packages/query-core/src/__tests__/mutations.test-d.tsx @@ -46,6 +46,54 @@ describe('MutateFunction', () => { }) }) + it('unknown variables', () => { + const mutate = {} as MutateFunction< + unknown, + DefaultError, + unknown, + unknown + > + + 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 @@ -110,6 +158,57 @@ describe('MutateFunction', () => { }) }) + 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< diff --git a/packages/vue-query/src/useMutation.ts b/packages/vue-query/src/useMutation.ts index 9c49730a79f..8fbdeecf2f8 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 }) } From 859ab7b269ea50e68964a71dad02fe797a3fd93e Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 07:45:17 +0000 Subject: [PATCH 5/9] ci: apply automated fixes --- packages/query-core/src/__tests__/mutations.test-d.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/query-core/src/__tests__/mutations.test-d.tsx b/packages/query-core/src/__tests__/mutations.test-d.tsx index d8f0d49370a..0a4423c3a1b 100644 --- a/packages/query-core/src/__tests__/mutations.test-d.tsx +++ b/packages/query-core/src/__tests__/mutations.test-d.tsx @@ -47,12 +47,7 @@ describe('MutateFunction', () => { }) it('unknown variables', () => { - const mutate = {} as MutateFunction< - unknown, - DefaultError, - unknown, - unknown - > + const mutate = {} as MutateFunction expectTypeOf[0]>().toEqualTypeOf() @@ -202,8 +197,7 @@ describe('MutateFunction', () => { >() expectTypeOf[1]>().toEqualTypeOf< - | undefined - | MutateOptions + undefined | MutateOptions >() mutate() // can be called with no arguments From 2f1b61251d4dfb6867514fa78c7af5382a676121 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Sat, 22 Aug 2026 10:10:29 +0200 Subject: [PATCH 6/9] angular --- .../angular-query-experimental/src/inject-mutation.ts | 8 ++++++-- packages/query-core/src/__tests__/mutations.test-d.tsx | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/angular-query-experimental/src/inject-mutation.ts b/packages/angular-query-experimental/src/inject-mutation.ts index 7eb605047f3..8acad495329 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/query-core/src/__tests__/mutations.test-d.tsx b/packages/query-core/src/__tests__/mutations.test-d.tsx index 0a4423c3a1b..53465d601cf 100644 --- a/packages/query-core/src/__tests__/mutations.test-d.tsx +++ b/packages/query-core/src/__tests__/mutations.test-d.tsx @@ -1,5 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest' -import type { DefaultError, MutateFunction, MutateOptions } from 'src/types' + +import type { DefaultError, MutateFunction, MutateOptions } from '../types' describe('MutateFunction', () => { it('void variables', () => { From 9b5509f38a3eb29954a385e4c9076a138ebcf7c5 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Sat, 22 Aug 2026 10:58:50 +0200 Subject: [PATCH 7/9] fix adapter signatures --- .../lit-query/src/createMutationController.ts | 18 +++++++++--------- packages/preact-query/src/useMutation.ts | 6 ++++-- packages/react-query/src/useMutation.ts | 6 ++++-- packages/solid-query/src/useMutation.ts | 4 ++-- .../svelte-query/src/createMutation.svelte.ts | 14 ++++++++------ 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/packages/lit-query/src/createMutationController.ts b/packages/lit-query/src/createMutationController.ts index d3133982ddd..1d295ce4709 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' @@ -44,10 +44,9 @@ export type MutationResultAccessor = * * Throws synchronously if no `QueryClient` can be resolved. */ - mutate: ( - variables: TVariables, - options?: MutateOptions, - ) => void + mutate: (...args: Parameters< + MutateFunction + >) => void /** * Starts the mutation and returns the observer promise. * @@ -190,14 +189,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 +212,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 4612aacc263..46322163765 100644 --- a/packages/preact-query/src/useMutation.ts +++ b/packages/preact-query/src/useMutation.ts @@ -52,8 +52,10 @@ 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/react-query/src/useMutation.ts b/packages/react-query/src/useMutation.ts index 2c66eb8ba8d..9dce38e1811 100644 --- a/packages/react-query/src/useMutation.ts +++ b/packages/react-query/src/useMutation.ts @@ -52,8 +52,10 @@ 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 2a2a8596520..2d19678baf1 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 51ff74827a1..7dbd44930ce 100644 --- a/packages/svelte-query/src/createMutation.svelte.ts +++ b/packages/svelte-query/src/createMutation.svelte.ts @@ -46,12 +46,14 @@ export function createMutation< observer.setOptions(options()) }) - const mutate = >(( - variables, - mutateOptions, - ) => { - observer.mutate(variables, mutateOptions).catch(noop) - }) + const mutate: CreateMutateFunction< + TData, + TError, + TVariables, + TContext + > = (...args) => { + observer.mutate(args[0] as TVariables, args[1]).catch(noop) + } let result = $state(observer.getCurrentResult()) watchChanges( From 758160c35c4cd6d7bf04fe8c831c887b4578a16f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 09:01:22 +0000 Subject: [PATCH 8/9] ci: apply automated fixes --- packages/lit-query/src/createMutationController.ts | 8 +++++--- packages/preact-query/src/useMutation.ts | 8 +++++--- packages/react-query/src/useMutation.ts | 8 +++++--- packages/svelte-query/src/createMutation.svelte.ts | 9 +++------ 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/lit-query/src/createMutationController.ts b/packages/lit-query/src/createMutationController.ts index 1d295ce4709..3dc5ca345e7 100644 --- a/packages/lit-query/src/createMutationController.ts +++ b/packages/lit-query/src/createMutationController.ts @@ -44,9 +44,11 @@ export type MutationResultAccessor = * * Throws synchronously if no `QueryClient` can be resolved. */ - mutate: (...args: Parameters< - MutateFunction - >) => void + mutate: ( + ...args: Parameters< + MutateFunction + > + ) => void /** * Starts the mutation and returns the observer promise. * diff --git a/packages/preact-query/src/useMutation.ts b/packages/preact-query/src/useMutation.ts index 46322163765..495aaadd910 100644 --- a/packages/preact-query/src/useMutation.ts +++ b/packages/preact-query/src/useMutation.ts @@ -52,9 +52,11 @@ export function useMutation< const mutate = useCallback< UseMutateFunction >( - (...args: Parameters< - UseMutateFunction - >) => { + ( + ...args: Parameters< + UseMutateFunction + > + ) => { observer.mutate(args[0] as TVariables, args[1]).catch(noop) }, [observer], diff --git a/packages/react-query/src/useMutation.ts b/packages/react-query/src/useMutation.ts index 9dce38e1811..240c6f70d88 100644 --- a/packages/react-query/src/useMutation.ts +++ b/packages/react-query/src/useMutation.ts @@ -52,9 +52,11 @@ export function useMutation< const mutate = React.useCallback< UseMutateFunction >( - (...args: Parameters< - UseMutateFunction - >) => { + ( + ...args: Parameters< + UseMutateFunction + > + ) => { observer.mutate(args[0] as TVariables, args[1]).catch(noop) }, [observer], diff --git a/packages/svelte-query/src/createMutation.svelte.ts b/packages/svelte-query/src/createMutation.svelte.ts index 7dbd44930ce..9bff1c5614a 100644 --- a/packages/svelte-query/src/createMutation.svelte.ts +++ b/packages/svelte-query/src/createMutation.svelte.ts @@ -46,12 +46,9 @@ export function createMutation< observer.setOptions(options()) }) - const mutate: CreateMutateFunction< - TData, - TError, - TVariables, - TContext - > = (...args) => { + const mutate: CreateMutateFunction = ( + ...args + ) => { observer.mutate(args[0] as TVariables, args[1]).catch(noop) } From 0c5154587cccafa8cd3279ba3032548679613376 Mon Sep 17 00:00:00 2001 From: TkDodo Date: Sat, 22 Aug 2026 11:23:32 +0200 Subject: [PATCH 9/9] changeset --- .changeset/optional-mutate-parameters.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/optional-mutate-parameters.md diff --git a/.changeset/optional-mutate-parameters.md b/.changeset/optional-mutate-parameters.md new file mode 100644 index 00000000000..0ca970acd95 --- /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`