From b56dd6cce5949658ada400a598bf534e92665aad Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Thu, 20 Aug 2026 11:48:35 +0000 Subject: [PATCH] docs(vue-query): explain why reactive() is needed for result narrowing The type narrowing example wraps useQuery() in reactive() without saying why, so destructuring the result directly and then checking isSuccess looks like it should work (#9244). Spell out that reactive() is what flattens the refs into one discriminated union, and show the data.value !== undefined check as the alternative when reactive() is not an option. Pin both shapes with type tests: the non-reactive result exposes Ref | Ref, and narrowing on data.value refines the ref itself. --- docs/framework/vue/typescript.md | 16 ++++++++++++ .../src/__tests__/useQuery.test-d.ts | 26 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/docs/framework/vue/typescript.md b/docs/framework/vue/typescript.md index 7cf70dbc4f3..a660e6e4e3c 100644 --- a/docs/framework/vue/typescript.md +++ b/docs/framework/vue/typescript.md @@ -68,6 +68,22 @@ if (isSuccess) { [typescript playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgVwM4FMCKz1QJ5wC+cAZlBCHAOQACMAhgHaoMDGA1gPQBuOAtAEcc+KgFgAUKEixEcKOnqsYwbuiKlylKr3RUA3BImsIzeEgAm9BgBo4wVAGVkrVulSp1AXjkKlK9AAUaFjCeAEA2lQwbjBUALq2AQCUcJ4AfHAACpr26AB08qgQADaqAQCsSVWGkiRwAfZOLm6oKQgScJ1wlgwSnJydAHoA-BKEEkA) +The `reactive()` wrapper is what makes this work: it flattens the result into +plain values, so `isSuccess` and `data` stay part of the same discriminated +union. Destructuring `useQuery()` directly gives you independent refs, and +TypeScript cannot carry a narrowing from one ref to another — `if (isSuccess.value)` +leaves `data.value` as `Group[] | undefined`. Without `reactive()`, narrow the +value ref itself: + +```tsx +const { data } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups }) + +if (data.value !== undefined) { + data.value + // ^? const data: Group[] +} +``` + [//]: # 'TypeNarrowing' [//]: # 'TypingError' diff --git a/packages/vue-query/src/__tests__/useQuery.test-d.ts b/packages/vue-query/src/__tests__/useQuery.test-d.ts index 7bb8cd8407b..5b7cc172b8a 100644 --- a/packages/vue-query/src/__tests__/useQuery.test-d.ts +++ b/packages/vue-query/src/__tests__/useQuery.test-d.ts @@ -2,6 +2,7 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { computed, reactive, ref } from 'vue-demi' import { queryKey, sleep } from '@tanstack/query-test-utils' import { queryOptions, useQuery } from '..' +import type { Ref } from 'vue-demi' import type { OmitKeyof, UseQueryOptions, UseQueryReturnType } from '..' describe('useQuery', () => { @@ -289,6 +290,31 @@ describe('useQuery', () => { expectTypeOf(query.error).toEqualTypeOf() } }) + + it('data should be a union of refs without reactive()', () => { + const key = queryKey() + + const query = useQuery({ + queryKey: key, + queryFn: () => sleep(0).then(() => 'Some data'), + }) + + expectTypeOf(query.data).toEqualTypeOf | Ref>() + }) + + it('data.value should narrow on an undefined check without reactive()', () => { + const key = queryKey() + + const { data } = useQuery({ + queryKey: key, + queryFn: () => sleep(0).then(() => 'Some data'), + }) + + if (data.value !== undefined) { + expectTypeOf(data.value).toEqualTypeOf() + expectTypeOf(data).toEqualTypeOf>() + } + }) }) describe('accept ref options', () => {