Skip to content

fix(vue-query): preserve TQueryKey inference with generic params (#8199)#10584

Open
ousamabenyounes wants to merge 2 commits intoTanStack:mainfrom
ousamabenyounes:fix/issue-8199
Open

fix(vue-query): preserve TQueryKey inference with generic params (#8199)#10584
ousamabenyounes wants to merge 2 commits intoTanStack:mainfrom
ousamabenyounes:fix/issue-8199

Conversation

@ousamabenyounes
Copy link
Copy Markdown
Contributor

@ousamabenyounes ousamabenyounes commented Apr 25, 2026

Summary

Fixes #8199.

UseQueryOptions wraps every property of QueryObserverOptions in MaybeRefDeep<…DeepUnwrapRef<TQueryKey>…>. For the queryKey property that produced an inference-hostile type — the recursive DeepUnwrapRef over a generic TQueryKey is opaque to TypeScript's homomorphic mapped-type inference, so TQueryKey collapsed to its default (QueryKey) whenever useQuery was called inside a composable that propagated a generic into the key.

This PR special-cases the queryKey property in the mapped type so it is wrapped only in a single-level MaybeRef<TQueryKey>. Inference now binds TQueryKey to the supplied key shape, while every other property keeps the existing MaybeRefDeep<…DeepUnwrapRef<TQueryKey>…> mapping (so queryFn's queryKey parameter is still the unwrapped form).

Repro from the issue (now compiles cleanly and types as UseQueryReturnType<'apple' | 'broccoli', Error>):

function useBasket<T extends 'fruit' | 'vegetable'>(type: T) {
  return useQuery({
    queryKey: ['basket', type] as const,
    queryFn({ queryKey: [, type] }) {
      return getBasket(type)
    },
  })
}

The same code already worked in react-query; this aligns vue-query behavior with it.

Verification

  • New type test in useQuery.test-d.ts reproducing the exact code from the issue
  • pnpm exec nx run @tanstack/vue-query:test:types — passes on TS 5.4 → 6.0
  • pnpm test:lib — vue 2 / 2.7 / 3 all 290/290 pass, 0 regressions
  • pnpm exec nx run @tanstack/vue-query:test:eslint — no errors
  • Existing queryKey: [...key, refValue] (refs as elements) and queryKey: ref([...]) (ref-wrapped key) patterns still typecheck
  • Changeset added (patch for @tanstack/vue-query)

Generated by Ora Studio
Vibe coded by ousamabenyounes

Summary by CodeRabbit

  • Bug Fixes

    • Restored correct type inference when using query operations inside generic composables, improving type safety and developer experience without runtime changes.
  • Tests

    • Added compile-time tests to verify type propagation for wrapped query usages.
  • Chores

    • Added a changeset to publish a patch release documenting the type-inference fix.

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02d881f1-0de0-43cf-b56e-a99793d70ba5

📥 Commits

Reviewing files that changed from the base of the PR and between beb9b81 and 2046279.

📒 Files selected for processing (3)
  • .changeset/vue-query-generic-querykey-inference.md
  • packages/vue-query/src/__tests__/useQuery.test-d.ts
  • packages/vue-query/src/useQuery.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/vue-query-generic-querykey-inference.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/vue-query/src/useQuery.ts

📝 Walkthrough

Walkthrough

Modifies vue-query types and adds a type test and changeset: UseQueryOptions now wraps queryKey with MaybeRef<TQueryKey> (instead of MaybeRefDeep<DeepUnwrapRef<TQueryKey>>), preserving generic TQueryKey inference when useQuery is wrapped by a generic composable.

Changes

Preserve TQueryKey inference

Layer / File(s) Summary
Core Type Definition
packages/vue-query/src/useQuery.ts
Update UseQueryOptions mapped type: treat queryKey as MaybeRef<QueryObserverOptions<...>['queryKey']> using TQueryKey directly; other properties remain MaybeRefDeep<DeepUnwrapRef<TQueryKey>>.
Type Tests
packages/vue-query/src/__tests__/useQuery.test-d.ts
Import UseQueryReturnType and add a compile-time test that useBasket<T> (a generic composable wrapping useQuery) preserves the inferred UseQueryReturnType for the generic key values.
Changeset
.changeset/vue-query-generic-querykey-inference.md
Add changeset marking a patch release and noting the fix for TQueryKey inference when useQuery is wrapped by a generic composable.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 I nudge the keys with gentle cheer,
Generics now whisper loud and clear.
No more unknowns in query arrays,
Types prance free through composable ways. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: preserving TQueryKey type inference when useQuery is used inside a generic composable with generic parameters, directly addressing issue #8199.
Description check ✅ Passed The PR description provides a comprehensive summary of changes, technical reasoning, reproduction example, and verification steps. It follows the repository template structure with clear explanations of the fix and testing performed.
Linked Issues check ✅ Passed The PR fully addresses issue #8199 by special-casing the queryKey property wrapper to preserve TQueryKey inference when useQuery is called inside generic composables, matching react-query behavior and resolving the type inference problem.
Out of Scope Changes check ✅ Passed All changes are directly in scope: the changeset documents the patch release, the test file adds a regression test for the specific issue, and the type mapping change directly addresses the queryKey inference problem without introducing unrelated modifications.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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 2046279

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

☁️ Nx Cloud last updated this comment at 2026-05-07 08:43:47 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@10584

@tanstack/eslint-plugin-query

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

@tanstack/preact-query

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

@tanstack/preact-query-devtools

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

@tanstack/preact-query-persist-client

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

@tanstack/query-async-storage-persister

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

@tanstack/query-broadcast-client-experimental

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

@tanstack/query-core

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

@tanstack/query-devtools

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

@tanstack/query-persist-client-core

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

@tanstack/query-sync-storage-persister

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

@tanstack/react-query

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

@tanstack/react-query-devtools

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

@tanstack/react-query-next-experimental

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

@tanstack/react-query-persist-client

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

@tanstack/solid-query

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

@tanstack/solid-query-devtools

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

@tanstack/solid-query-persist-client

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

@tanstack/svelte-query

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

@tanstack/svelte-query-devtools

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

@tanstack/svelte-query-persist-client

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

@tanstack/vue-query

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

@tanstack/vue-query-devtools

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

commit: 2046279

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.

🧹 Nitpick comments (1)
packages/vue-query/src/__tests__/useQuery.test-d.ts (1)

155-174: Strengthen the regression assertion.

assertType(useBasket('fruit')) only verifies the expression compiles. The original bug per #8199 was that TQueryKey fell back to unknown/QueryKey and the result was typed as UseQueryReturnType<unknown, Error> — that broader form would also compile here. To lock the regression, assert the propagated TData so a future regression to unknown would fail.

♻️ Proposed stronger assertion
-      assertType(useBasket('fruit'))
+      const query = reactive(useBasket('fruit'))
+      expectTypeOf(query.data).toEqualTypeOf<'apple' | 'broccoli' | undefined>()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vue-query/src/__tests__/useQuery.test-d.ts` around lines 155 - 174,
Replace the weak compile-only assertion assertType(useBasket('fruit')) with a
concrete type assertion that verifies the propagated TData; e.g.
assertType<UseQueryReturnType<'apple', Error>>(useBasket('fruit')). Update the
test to reference useBasket and getBasket so it explicitly expects the data type
'apple' (not unknown), using the UseQueryReturnType generic to lock the
regression.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/vue-query/src/__tests__/useQuery.test-d.ts`:
- Around line 155-174: Replace the weak compile-only assertion
assertType(useBasket('fruit')) with a concrete type assertion that verifies the
propagated TData; e.g. assertType<UseQueryReturnType<'apple',
Error>>(useBasket('fruit')). Update the test to reference useBasket and
getBasket so it explicitly expects the data type 'apple' (not unknown), using
the UseQueryReturnType generic to lock the regression.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 997560b3-0722-4838-94f9-598c1ceb3889

📥 Commits

Reviewing files that changed from the base of the PR and between 441204b and 25f32fb.

📒 Files selected for processing (3)
  • .changeset/vue-query-generic-querykey-inference.md
  • packages/vue-query/src/__tests__/useQuery.test-d.ts
  • packages/vue-query/src/useQuery.ts

@ousamabenyounes
Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — addressed the nitpick in beb9b81.

  • Strengthen regression assertion (packages/vue-query/src/__tests__/useQuery.test-d.ts:172): replaced bare assertType(useBasket('fruit')) with assertType<UseQueryReturnType<'apple' | 'broccoli', Error>>(useBasket('fruit')). The bare form only checked the expression compiled, which still held under the old buggy behavior (TQueryKey collapsing to QueryKey, TData inferred as unknown). Pinning the propagated TData locks the regression — any future drift to unknown will now fail the type test.

Verified locally:

  • pnpm exec nx run @tanstack/vue-query:test:types — passes on TS 5.4 → 6.0
  • pnpm exec nx run @tanstack/vue-query:test:lib — 290/290 vitest tests pass (incl. typecheck)
  • pnpm exec nx run @tanstack/vue-query:test:eslint — clean

@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 exactly the right strengthening — pinning TData to 'apple' | 'broccoli' turns the assertion into a genuine regression guard. Under the old behavior where TQueryKey collapsed to QueryKey and TData fell back to unknown, the new form would now correctly fail the type check. Great work on the thorough local verification across TS 5.4–6.0 as well! 🎉

(´• ω •`) ♡
∪ ∪


🧠 Learnings used
Learnt from: ousamabenyounes
Repo: TanStack/query PR: 0
File: :0-0
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()`.

Learnt from: sukvvon
Repo: TanStack/query PR: 9892
File: packages/solid-query-persist-client/src/__tests__/PersistQueryClientProvider.test.tsx:331-335
Timestamp: 2025-11-22T09:06:05.219Z
Learning: In TanStack/query test files, when a queryFn contains side effects (e.g., setting flags for test verification), prefer async/await syntax for clarity; when there are no side effects, prefer the .then() pattern for conciseness.

Learnt from: Newbie012
Repo: TanStack/query PR: 10295
File: examples/react/eslint-plugin-demo/package.json:8-16
Timestamp: 2026-03-19T09:24:51.614Z
Learning: In TanStack/query, example packages under `examples/` consistently use semver ranges (e.g., `"^5.x.y"`) for TanStack dependencies such as `tanstack/react-query` and `tanstack/eslint-plugin-query` in their `package.json` files, rather than `workspace:*` refs. Do not suggest changing these to workspace refs on the grounds of consistency.

Learnt from: Newbie012
Repo: TanStack/query PR: 10295
File: .changeset/exhaustive-deps-allowlist.md:2-5
Timestamp: 2026-03-19T09:24:23.392Z
Learning: In TanStack/query, breaking behavioral changes to `tanstack/eslint-plugin-query` ESLint rules (e.g., the `exhaustive-deps` rule reporting more granularly) are intentionally versioned as `minor` rather than `major`. The rationale (discussed in https://github.com/TanStack/query/issues/6853#issuecomment-3405204044) is that ESLint rules are opt-in, and escape hatches (such as `allowlist` options) are provided to handle false positives. Do not flag such changeset entries as requiring a major bump.

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.

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.

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 May 7, 2026 08:01
…Stack#8199)

The mapped UseQueryOptions type wrapped queryKey in
MaybeRefDeep<DeepUnwrapRef<TQueryKey>>. The recursive DeepUnwrapRef on a
generic TQueryKey is opaque to TS's homomorphic mapped type inference,
so TQueryKey collapsed to its default (QueryKey) when useQuery was
called from a composable that propagates a generic into the queryKey.

For the queryKey property only, drop DeepUnwrapRef and use a single-
level MaybeRef<TQueryKey> wrap. Inference now binds TQueryKey to the
supplied queryKey shape; queryFn still receives DeepUnwrapRef<TQueryKey>
via the existing mapping for non-queryKey properties.

Co-Authored-By: Ora Studio <noreply@oratelecom.net>
…xplicit TData

Per CodeRabbit feedback: bare assertType(useBasket('fruit')) only verified
the call compiled, which would still pass under the old buggy behavior
(TQueryKey collapsing to QueryKey, TData inferred as unknown). Lock the
regression by asserting the propagated TData explicitly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[vue-query] type error with generic query params

1 participant