From 2b87d91dbfd3afe3886193999fe7f48030bf18c2 Mon Sep 17 00:00:00 2001 From: byungsker <72309817+byungsker@users.noreply.github.com> Date: Sat, 25 Apr 2026 00:54:16 +0900 Subject: [PATCH 1/2] fix(vue-query): preserve branded types in MaybeRefDeep Branded types (string & { __brand: string }) were being recursively mapped by MaybeRefDeep, causing type inference failures when used in queryKey tuples passed from queryOptions to useQuery. Fixes #9920 --- .changeset/branded-types-fix.md | 7 +++ .../src/__tests__/queryOptions.test-d.ts | 44 +++++++++++++++++++ packages/vue-query/src/types.ts | 12 ++--- 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 .changeset/branded-types-fix.md diff --git a/.changeset/branded-types-fix.md b/.changeset/branded-types-fix.md new file mode 100644 index 00000000000..514dfda891f --- /dev/null +++ b/.changeset/branded-types-fix.md @@ -0,0 +1,7 @@ +--- +"@tanstack/vue-query": patch +--- + +fix(vue-query): preserve branded types in MaybeRefDeep + +Branded types (e.g. `string & { __brand: 'PostId' }`) were being recursively mapped by `MaybeRefDeep`, causing TypeScript inference failures when used in `queryKey` tuples passed from `queryOptions` to `useQuery`. This fix adds a terminal branch to `MaybeRefDeep` so that branded types are preserved as-is instead of having their properties unwrapped. diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index 481dd808fc5..bce3be4e8dd 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -298,4 +298,48 @@ describe('queryOptions', () => { expectTypeOf(options.queryKey).not.toBeUndefined() }) + + it('should work with branded queryKey', () => { + type PostId = string & { readonly __brand: 'PostId' } + const postId = '123' as PostId + + const options = queryOptions({ + queryKey: ['post', postId], + queryFn: () => Promise.resolve({ id: postId }), + }) + + expectTypeOf(options.queryKey).not.toBeUndefined() + + // This should not error - the branded queryKey should be accepted by useQuery + const { data } = reactive(useQuery(options)) + expectTypeOf(data).toEqualTypeOf<{ id: PostId } | undefined>() + }) + + it('should work with branded queryKey inside MaybeRefOrGetter', () => { + type PostId = string & { readonly __brand: 'PostId' } + const postId = '123' as PostId + + // Test 1: simple queryOptions with branded string directly in tuple (like existing test) + const simpleOptions = queryOptions({ + queryKey: ['post', postId as PostId], + queryFn: () => Promise.resolve({ id: postId as PostId }), + }) + const { data: simpleData } = reactive(useQuery(simpleOptions)) + expectTypeOf(simpleData).toEqualTypeOf<{ id: PostId } | undefined>() + + // Test 2: branded string inside object inside tuple + const nestedOptions = queryOptions({ + queryKey: ['post', { postId: postId as PostId }], + queryFn: () => Promise.resolve({ id: postId as PostId }), + }) + const { data: nestedData } = reactive(useQuery(nestedOptions)) + expectTypeOf(nestedData).toEqualTypeOf<{ id: PostId } | undefined>() + + // Test 3: inline branded queryKey + const { data: inlineData } = reactive(useQuery({ + queryKey: ['post', { postId: postId as PostId }], + queryFn: () => Promise.resolve({ id: postId as PostId }), + })) + expectTypeOf(inlineData).toEqualTypeOf<{ id: PostId } | undefined>() + }) }) diff --git a/packages/vue-query/src/types.ts b/packages/vue-query/src/types.ts index 8ef5664b29f..6879b1c323c 100644 --- a/packages/vue-query/src/types.ts +++ b/packages/vue-query/src/types.ts @@ -31,11 +31,13 @@ export type MaybeRefOrGetter = MaybeRef | (() => T) export type MaybeRefDeep = MaybeRef< T extends Function ? T - : T extends object - ? { - [Property in keyof T]: MaybeRefDeep - } - : T + : T extends { __brand: infer _ } + ? T + : T extends object + ? { + [Property in keyof T]: MaybeRefDeep + } + : T > export type NoUnknown = Equal extends true ? never : T From df73bb957ce3a22d79171b09e2d894bbfaa40fc8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 25 Apr 2026 04:56:20 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .../vue-query/src/__tests__/queryOptions.test-d.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index bce3be4e8dd..31226b94a36 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -336,10 +336,12 @@ describe('queryOptions', () => { expectTypeOf(nestedData).toEqualTypeOf<{ id: PostId } | undefined>() // Test 3: inline branded queryKey - const { data: inlineData } = reactive(useQuery({ - queryKey: ['post', { postId: postId as PostId }], - queryFn: () => Promise.resolve({ id: postId as PostId }), - })) + const { data: inlineData } = reactive( + useQuery({ + queryKey: ['post', { postId: postId as PostId }], + queryFn: () => Promise.resolve({ id: postId as PostId }), + }), + ) expectTypeOf(inlineData).toEqualTypeOf<{ id: PostId } | undefined>() }) })