Skip to content

fix(vue-query): preserve discriminated union narrowing in UseBaseQueryReturnType (#9244)#10580

Open
ousamabenyounes wants to merge 4 commits intoTanStack:mainfrom
ousamabenyounes:fix/issue-9244
Open

fix(vue-query): preserve discriminated union narrowing in UseBaseQueryReturnType (#9244)#10580
ousamabenyounes wants to merge 4 commits intoTanStack:mainfrom
ousamabenyounes:fix/issue-9244

Conversation

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@ousamabenyounes ousamabenyounes commented Apr 25, 2026

Summary

Fixes #9244

The current UseBaseQueryReturnType mapped type already produces a discriminated union when instantiated with a generic TResult (homomorphic mapped types preserve unions over generic parameters), so narrowing via reactive(useQuery(...)) already works as documented. However:

  • The intent — that the type must distribute over each variant of QueryObserverResult — is implicit and easy to break in future refactors.
  • The narrowing limitations under common user patterns (destructuring without reactive(), accessing .value on a wrapped discriminator) are not captured by tests, so users (narrowing typescript keeps data as possible undefined #9244) hit them without a clear answer.

This PR:

  • Makes the distributive intent of UseBaseQueryReturnType explicit (TResult extends unknown ? ... : never). No runtime change; same observable type for all currently-supported usages.
  • Adds 4 focused type tests under useQuery.test-d.tsissue #9244 — narrowing without reactive():
    • data shape on raw useQuery() matches the docs (Ref<T> | Ref<undefined>).
    • if (data.value !== undefined) correctly narrows data to Ref<T> and data.value to T (the recommended pattern when reactive() is not available).
    • reactive() preserves narrowing across destructured { data, isSuccess } and across the status === 'success' discriminator.
  • Adds a callout to docs/framework/vue/typescript.md next to the existing narrowing example explaining when reactive() is required and what to do otherwise.

Why this isn't a deeper type-level fix

I traced through every plausible structural change (distributing over the conditional, plain-bool discriminators, ToRefs-based variants, etc.). Cross-property narrowing without reactive() requires either (a) breaking the API by removing Ref<> from discriminator props, which would break all existing .value access at call sites, or (b) a TypeScript-level feature for narrowing through Ref.value that doesn't exist. reactive() is the supported escape hatch and now the docs say so directly.

Verification

  • Baseline tests on main: all 289 vue-query tests pass.
  • Post-fix: 293 pass, 0 regressions, 4 new type tests added — pnpm exec vitest run --typecheck.enabled clean.
  • pnpm exec nx run-many -t test:types -p @tanstack/vue-query @tanstack/query-core @tanstack/react-query — green across TS 5.4–6.0.
  • pnpm exec nx run @tanstack/vue-query:test:lib and :test:eslint — green.
  • Changeset added (patch for @tanstack/vue-query).

Files changed

File Change
packages/vue-query/src/useBaseQuery.ts Make UseBaseQueryReturnType explicitly distributive; pin suspense return to the un-distributed QueryObserverResult<TData, TError> so its signature doesn't shift.
packages/vue-query/src/__tests__/useQuery.test-d.ts 4 new type tests covering the destructure-without-reactive scenario from the issue + the recommended workarounds.
docs/framework/vue/typescript.md Note explaining when reactive() is required and the data.value !== undefined workaround.
.changeset/vue-query-narrow-result-type.md Patch changeset.

Generated by Ora Studio
Vibe coded by ousamabenyounes

Summary by CodeRabbit

  • Bug Fixes

    • Preserve discriminated-union narrowing so TypeScript refines query result types correctly (supports narrowing with reactive() and via direct data.value !== undefined checks).
  • Documentation

    • Added guidance on narrowing patterns for query results with and without reactive().
  • Tests

    • Added regression tests validating the described TypeScript narrowing behaviors.
  • Chores

    • Added a changeset entry for a patch release.

Vibe Coded by Ousama Ben Younes
Developed With Ora Studio (Claude Code)

…yReturnType (TanStack#9244)

Make the mapped type explicitly distributive over each variant of
QueryObserverResult, and lock in the narrowing patterns that work
without reactive() (direct data.value !== undefined check) versus
those that require reactive() (narrowing via isSuccess / status).

Fixes TanStack#9244

Generated by Claude Code
Vibe coded by ousamabenyounes

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation package: vue-query labels Apr 25, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

Converts UseBaseQueryReturnType to a distributive conditional type to preserve discriminated-union narrowing for Vue Query results; adds a changeset, documentation clarifying when reactive() is required for cross-property narrowing, and type regression tests validating the behavior.

Changes

Cohort / File(s) Summary
Changeset
\.changeset/vue-query-narrow-result-type.md
Adds a patch changeset describing the fix to UseBaseQueryReturnType (make mapped type distributive) and documents which narrowing approaches require reactive() vs. those that don't.
Docs
docs/framework/vue/typescript.md
Documents that discriminator-driven narrowing of destructured useQuery results requires wrapping the result with reactive(), and that direct data.value !== undefined checks work without reactive().
Type tests
packages/vue-query/src/__tests__/useQuery.test-d.ts
Adds regression/type tests for issue #9244 covering narrowing behavior both without and with reactive(); asserts expected discriminated-union narrowing and suspense() promise behavior.
Type definition
packages/vue-query/src/useBaseQuery.ts
Changes UseBaseQueryReturnType from a single mapped type over TResult to a distributive conditional type (TResult extends unknown ? ... : never) so union branches preserve discriminators; suspense signature unchanged.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A tiny hop to mend the types today,
Discriminators learned to find their way.
Wrap in reactive() when properties must align,
Or check data.value !== undefined and you're fine.
Hooray — type carrots gathered, neat and gay.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing discriminated union narrowing in UseBaseQueryReturnType for issue #9244.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #9244 by making UseBaseQueryReturnType explicitly distributive, adding type tests covering the narrowing scenarios, and documenting the workarounds (reactive() and data.value checks).
Out of Scope Changes check ✅ Passed All changes directly support the stated objectives: type definition fix, regression tests, documentation clarification, and changeset entry. No extraneous modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description comprehensively covers the changes, rationale, and verification steps. All required template sections are completed with clear detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 25, 2026

View your CI Pipeline Execution ↗ for commit 2163cc3

Command Status Duration Result
nx run-many --target=build --exclude=examples/*... ✅ Succeeded <1s View ↗
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 17s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-28 15:54:09 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 25, 2026

More templates

@tanstack/angular-query-experimental

npm i https://pkg.pr.new/@tanstack/angular-query-experimental@10580

@tanstack/eslint-plugin-query

npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@10580

@tanstack/preact-query

npm i https://pkg.pr.new/@tanstack/preact-query@10580

@tanstack/preact-query-devtools

npm i https://pkg.pr.new/@tanstack/preact-query-devtools@10580

@tanstack/preact-query-persist-client

npm i https://pkg.pr.new/@tanstack/preact-query-persist-client@10580

@tanstack/query-async-storage-persister

npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@10580

@tanstack/query-broadcast-client-experimental

npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@10580

@tanstack/query-core

npm i https://pkg.pr.new/@tanstack/query-core@10580

@tanstack/query-devtools

npm i https://pkg.pr.new/@tanstack/query-devtools@10580

@tanstack/query-persist-client-core

npm i https://pkg.pr.new/@tanstack/query-persist-client-core@10580

@tanstack/query-sync-storage-persister

npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@10580

@tanstack/react-query

npm i https://pkg.pr.new/@tanstack/react-query@10580

@tanstack/react-query-devtools

npm i https://pkg.pr.new/@tanstack/react-query-devtools@10580

@tanstack/react-query-next-experimental

npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@10580

@tanstack/react-query-persist-client

npm i https://pkg.pr.new/@tanstack/react-query-persist-client@10580

@tanstack/solid-query

npm i https://pkg.pr.new/@tanstack/solid-query@10580

@tanstack/solid-query-devtools

npm i https://pkg.pr.new/@tanstack/solid-query-devtools@10580

@tanstack/solid-query-persist-client

npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@10580

@tanstack/svelte-query

npm i https://pkg.pr.new/@tanstack/svelte-query@10580

@tanstack/svelte-query-devtools

npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@10580

@tanstack/svelte-query-persist-client

npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@10580

@tanstack/vue-query

npm i https://pkg.pr.new/@tanstack/vue-query@10580

@tanstack/vue-query-devtools

npm i https://pkg.pr.new/@tanstack/vue-query-devtools@10580

commit: 2163cc3

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/vue-query/src/useBaseQuery.ts`:
- Around line 38-49: UseBaseQueryReturnType currently hardcodes the suspense
return to Promise<QueryObserverResult<TData, TError>> which loses precision for
parameterized TResult (e.g., DefinedQueryObserverResult,
InfiniteQueryObserverResult); update the type so that the mapped properties
remain distributive over TResult but the suspense property is parameterized by
TResult (i.e., make suspense: () => Promise<TResult extends unknown ? TResult :
never> or lift suspense outside the distributive conditional) so awaiting
suspense() returns the actual TResult shape (preserving
fetchNextPage/hasNextPage/data non-undefined), and ensure this aligns with the
runtime behavior of
observer.getOptimisticResult/fetchOptimistic/getCurrentResult so no other
signatures need changing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9bd7d5bb-ab5a-4222-9ec2-4d0fe0de7e76

📥 Commits

Reviewing files that changed from the base of the PR and between f85d825 and 7042fa9.

📒 Files selected for processing (4)
  • .changeset/vue-query-narrow-result-type.md
  • docs/framework/vue/typescript.md
  • packages/vue-query/src/__tests__/useQuery.test-d.ts
  • packages/vue-query/src/useBaseQuery.ts

Comment thread packages/vue-query/src/useBaseQuery.ts Outdated
The previous shape pinned `suspense` to `Promise<QueryObserverResult<TData,
TError>>` inside the distributive conditional, so awaiting `suspense()` lost
type precision when `UseBaseQueryReturnType` was instantiated with
`DefinedQueryObserverResult` (data became `TData | undefined` again) or
`InfiniteQueryObserverResult` (no `fetchNextPage`/`hasNextPage`/`data.pages`).

Lifting `suspense` outside the distributive arm and typing it as
`Promise<TResult>` keeps the per-property mapping distributive but lets the
suspense result carry the parameterized observer type — matching the runtime
already returned by `observer.fetchOptimistic()` / `getOptimisticResult()`.
@ousamabenyounes
Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — addressed in b69f079. Lifted suspense outside the distributive conditional and typed it as Promise<TResult>, so awaiting suspense() now preserves the parameterized observer shape (DefinedQueryObserverResult keeps non-undefined data, InfiniteQueryObserverResult keeps fetchNextPage / hasNextPage / data.pages). Aligns with the runtime returned by observer.fetchOptimistic().

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

Tip

For best results, initiate chat on the files or code changes.

@ousamabenyounes That's a better approach — using Promise<TResult> rather than the pinned Promise<QueryObserverResult<TData, TError>> means callers working with specialized result variants (e.g. DefinedQueryObserverResult or InfiniteQueryObserverResult) get the correct, tighter type back from await suspense() without losing properties like data.pages or the non-undefined data guarantee. Thanks for the thorough explanation!


✏️ Learnings added
Learnt from: ousamabenyounes
URL: https://github.com/TanStack/query/pull/10580

Timestamp: 2026-04-25T22:35:09.256Z
Learning: In `packages/vue-query/src/useBaseQuery.ts`, the `suspense` method in `UseBaseQueryReturnType` should be declared **outside** the distributive conditional type and typed as `Promise<TResult>` (not pinned to `Promise<QueryObserverResult<TData, TError>>`). This preserves the parameterized observer shape for callers using specialized variants like `DefinedQueryObserverResult` (non-undefined `data`) and `InfiniteQueryObserverResult` (`fetchNextPage`/`hasNextPage`/`data.pages`), and aligns with the runtime value returned by `observer.fetchOptimistic()`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: byungsker
Repo: TanStack/query PR: 10465
File: packages/vue-query/src/__tests__/queryOptions.test-d.ts:292-300
Timestamp: 2026-04-13T15:15:51.168Z
Learning: In `packages/vue-query/src/queryOptions.ts`, the predicate form of `enabled` — i.e., `(query) => boolean` — is not supported by `queryOptions` (throws a TypeScript error). This is a pre-existing limitation that exists even on the main branch, unrelated to any specific PR. It is distinct from the simple getter form `() => boolean` which is supported. Predicate support in `queryOptions` would require a separate feature implementation.

Learnt from: oscartbeaumont
Repo: TanStack/query PR: 9564
File: packages/solid-query-devtools/src/production.tsx:2-3
Timestamp: 2025-08-19T03:18:18.303Z
Learning: In the solid-query-devtools package, the codebase uses a pattern of type-only default imports combined with typeof for component type annotations (e.g., `import type SolidQueryDevtoolsComp from './devtools'` followed by `typeof SolidQueryDevtoolsComp`). This pattern is consistently used across index.tsx and production.tsx files, and the maintainers prefer consistency over changing this approach.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

ousamabenyounes and others added 2 commits April 26, 2026 13:41
Adds a regression test asserting that `useQuery(...).suspense()` resolves to
`Promise<QueryObserverResult<TData, TError>>` (parameterized by `TResult` on
`UseBaseQueryReturnType`) and that awaiting it preserves the discriminated
union narrowing on `isSuccess` / `isError`. Closes the verification gap noted
on TanStack#10580 — the parameterization was previously only validated via tsc build,
not by an explicit `expectTypeOf` assertion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation package: vue-query

Projects

None yet

Development

Successfully merging this pull request may close these issues.

narrowing typescript keeps data as possible undefined

2 participants