From 73effd0641d8b9cd0109187f168e7acd0487c7b5 Mon Sep 17 00:00:00 2001 From: adityachaudhary99 Date: Sat, 22 Aug 2026 01:53:58 +0530 Subject: [PATCH] fix(vue-query): stop MaybeRefDeep stripping branded queryKey types (#9920) --- .changeset/fix-branded-types-mayberefdeep.md | 5 ++ .../src/__tests__/queryOptions.test-d.ts | 60 +++++++++++++++++++ packages/vue-query/src/types.ts | 12 ++-- 3 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 .changeset/fix-branded-types-mayberefdeep.md diff --git a/.changeset/fix-branded-types-mayberefdeep.md b/.changeset/fix-branded-types-mayberefdeep.md new file mode 100644 index 00000000000..ddb150109f6 --- /dev/null +++ b/.changeset/fix-branded-types-mayberefdeep.md @@ -0,0 +1,5 @@ +--- +'@tanstack/vue-query': patch +--- + +fix(vue-query): preserve branded types in `MaybeRefDeep` so branded `queryKey`s (e.g. `string & { __brand }`) pass through `queryOptions` into `useQuery` without a TS2769 overload error diff --git a/packages/vue-query/src/__tests__/queryOptions.test-d.ts b/packages/vue-query/src/__tests__/queryOptions.test-d.ts index 8b1ee84c1e6..5d35fac357f 100644 --- a/packages/vue-query/src/__tests__/queryOptions.test-d.ts +++ b/packages/vue-query/src/__tests__/queryOptions.test-d.ts @@ -362,4 +362,64 @@ 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 inlineData = reactive( + useQuery({ + queryKey: ['post', postId], + queryFn: () => Promise.resolve({ id: postId }), + }), + ) + + expectTypeOf(inlineData.data).toEqualTypeOf<{ id: PostId } | undefined>() + }) + + it('should still tag the queryKey with the queryFn result when it contains branded types', () => { + type PostId = string & { readonly __brand: 'PostId' } + const postId = '123' as PostId + + const { queryKey: tagged } = queryOptions({ + queryKey: ['post', { postId }], + queryFn: () => Promise.resolve({ id: postId }), + }) + + expectTypeOf(tagged[dataTagSymbol]).toEqualTypeOf<{ id: PostId }>() + }) }) 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