From af629b6e156266b8b6dd818c09cefd2e4dcac230 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] 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 | 42 +++++++++++++++++++ packages/vue-query/src/types.ts | 12 +++--- 3 files changed, 56 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 a3e8c6d0d41..c4cd53a9176 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -332,4 +332,46 @@ 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() + + 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 + + const simpleOptions = queryOptions({ + queryKey: ['post', postId], + queryFn: () => Promise.resolve({ id: postId }), + }) + const { data: simpleData } = reactive(useQuery(simpleOptions)) + expectTypeOf(simpleData).toEqualTypeOf<{ id: PostId } | undefined>() + + const nestedOptions = queryOptions({ + queryKey: ['post', { postId }], + queryFn: () => Promise.resolve({ id: postId }), + }) + const { data: nestedData } = reactive(useQuery(nestedOptions)) + expectTypeOf(nestedData).toEqualTypeOf<{ id: PostId } | undefined>() + + const { data: inlineData } = reactive( + useQuery({ + queryKey: ['post', { postId }], + queryFn: () => Promise.resolve({ id: 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