fix(angular-query): fix injectInfiniteQuery to narrow type
- fixes: #8984
- related: #9016
Cause
When using Omit, a union type is transformed into an object.
This causes BaseQueryNarrowing to behave unexpectedly and fail to narrow types correctly.
I believe this comment is related to the similar issue.
Problem
-
Using
OmitCurrently,
BaseQueryNarrowingonly works ininjectQuerywheninitialDatais not present. Repro example. -
Without
Omit
The narrowing works (see: #9016),
but then isSuccess becomes a strange intersection type:
((this: CreateBaseQueryResult) => this is CreateBaseQueryResult) & (() => false) & ...
As coderrabbitai pointed out, this can cause potential issues.
-
Using
DistributiveOmit
As suggested in a previous comment, using DistributiveOmit resolves the narrowing issue. However, it changes the inferred type of data as follows:
const { data } = injectQuery(() => ({
queryKey: ['key'],
queryFn: () => ({ wow: true }),
}))
// before
expectTypeOf(data).toEqualTypeOf<Signal<{ wow: boolean | undefined }>>()
// after (with DistributiveOmit)
expectTypeOf(data).toEqualTypeOf<Signal<{ wow: boolean }> | Signal<undefined>>()
I am not very familiar with Angular conventions, so I am not sure whether
Signal<{ wow: boolean }> | Signal<undefined> is considered valid.
This Pr
Fix BaseQueryNarrowing to narrow based on the data provided at the call site
I think we can use DistributiveOmit if Signal<{ wow: boolean }> | Signal<undefined> is considered an acceptable type in Angular, without changing the existing BaseQueryNarrowing behavior.
Let me know your thoughts.
Summary by CodeRabbit
-
New Features
- More precise TypeScript typings for query and infinite query results, improving type narrowing for success, error, and pending states.
- injectQuery/injectInfiniteQuery now expose broader, more accurate return types for better DX.
-
Examples
- Infinite query example updated to clearly separate loading, error, and success UI states.
-
Tests
- Expanded coverage for initialData scenarios and error typing, including status-guarded assertions.
-
Refactor
- Generalized internal typing to consistently carry state across result types, enhancing status-guard behavior without changing the public API.
Walkthrough
Adds state-aware type-narrowing to Angular query result types and updates implementations/tests to expose and validate narrowed types for infinite and regular queries; also adjusts a template conditional structure. No runtime API surface changes beyond internal return-value casts.
Changes
| Cohort / File(s) | Summary |
|---|---|
Type system updatepackages/angular-query-experimental/src/types.ts |
Added CreateNarrowQueryResult and extended BaseQueryNarrowing to accept TState; updated CreateBaseQueryResult, DefinedCreateQueryResult, CreateInfiniteQueryResult, and DefinedCreateInfiniteQueryResult to include TState and map signals from OmitKeyof<TState, keyof BaseQueryNarrowing, 'safely'>. |
inject implementationspackages/angular-query-experimental/src/inject-infinite-query.ts, packages/angular-query-experimental/src/inject-query.ts |
Changed implementation return casts: injectInfiniteQuery now casts result to CreateInfiniteQueryResult (via unknown) and injectQuery implementation casts to `CreateQueryResult |
Type-narrowing testspackages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts, packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts |
Reorganized and expanded tests to assert isSuccess/isPending/isError narrowing for infinite queries (with and without initialData) and added an isError assertion for injectQuery initialData case. No runtime behavior changes. |
Example template changeexamples/angular/infinite-query-with-max-pages/src/app/components/example.component.html |
Replaced chained conditional (if/else-if/else) with separate @if blocks for loading, error, and success states (decoupling else branches). |
Sequence Diagram(s)
sequenceDiagram
autonumber
participant Component
participant injectFn as injectInfiniteQuery / injectQuery
participant Result as QueryResult (typed)
Component->>injectFn: call inject*Query(...)
Note right of injectFn: implementation casts runtime result\nto CreateQueryResult | DefinedCreateQueryResult\nor CreateInfiniteQueryResult
injectFn-->>Result: return typed wrapper
Component->>Result: call isSuccess() / isError() / isPending()
alt isSuccess
Note right of Result: Type narrows to success-shaped\nCreateNarrowQueryResult → data() is defined
else isError
Note right of Result: Type narrows to error-shaped\nCreateNarrowQueryResult → error() is defined
else isPending
Note right of Result: Type narrows to pending-shaped\nCreateNarrowQueryResult
end
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
- TanStack/query#9270 — Devtools relocation and export refactor (overlaps with type/export surface changes and reorganizations).
Poem
I nibble types in twilight code,
I hop where narrow signals go.
isSuccess—data springs, neat and bright,
isError—error’s clear in sight.
A rabbit cheers for types made right. 🐇✨
Pre-merge checks and finishing touches
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Out of Scope Changes Check | ⚠️ Warning | Most edits are focused on type-narrowing for injectInfiniteQuery, however there are a couple of edits that appear outside the linked issue scope: the example template file examples/angular/infinite-query-with-max-pages/src/app/components/example.component.html was refactored, and the injectQuery implementation return cast was broadened, neither of which are explicitly required by issue [#8984]. | Please either remove or split non-essential example UI changes into a separate PR and add a brief justification in this PR for the injectQuery return-cast change (or add tests demonstrating it is necessary); run the full type-check and CI after isolating or documenting these edits before merging. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title Check | ✅ Passed | The title is concise, specific, and accurately summarizes the primary change — fixing type narrowing for injectInfiniteQuery in the angular-query package — and it aligns with the PR objectives and the provided changeset (type updates, implementation cast, and tests). |
| Linked Issues Check | ✅ Passed | Based on the provided changeset, the PR implements the requested fix for the linked issue [#8984] by making BaseQueryNarrowing state-aware (new CreateNarrowQueryResult and TState generics), updating CreateInfiniteQueryResult/related types, adding tests that assert narrowing for injectInfiniteQuery (and an injectQuery test), and adjusting the injectInfiniteQuery implementation to return the new narrowed type, which together provide reproducible coverage of the narrowing behavior. |
| Docstring Coverage | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. |
✨ Finishing touches
- [ ] 📝 Generate Docstrings
🧪 Generate unit tests
- [ ] Create PR with unit tests
- [ ] Post copyable unit tests in a comment
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
View your CI Pipeline Execution ↗ for commit 29a7076a4c584b2870289b0185c935742d1bf182
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:sherif,test:knip,tes... |
✅ Succeeded | 3m 24s | View ↗ |
nx run-many --target=build --exclude=examples/*... |
✅ Succeeded | 9s | View ↗ |
☁️ Nx Cloud last updated this comment at 2025-09-21 01:06:29 UTC
More templates
- @tanstack/query-example-angular-auto-refetching
- @tanstack/query-example-angular-basic
- @tanstack/query-example-angular-basic-persister
- @tanstack/query-example-angular-devtools-panel
- @tanstack/query-example-angular-infinite-query-with-max-pages
- @tanstack/query-example-angular-optimistic-updates
- @tanstack/query-example-angular-pagination
- @tanstack/query-example-angular-query-options-from-a-service
- @tanstack/query-example-angular-router
- @tanstack/query-example-angular-rxjs
- @tanstack/query-example-angular-simple
- @tanstack/query-example-solid-astro
- @tanstack/query-example-solid-basic
- @tanstack/query-example-solid-basic-graphql-request
- @tanstack/query-example-solid-default-query-function
- @tanstack/query-example-solid-simple
- @tanstack/query-example-solid-start-streaming
- @tanstack/query-example-react-algolia
- @tanstack/query-example-react-auto-refetching
- @tanstack/query-example-react-basic
- @tanstack/query-example-react-basic-graphql-request
- @tanstack/query-example-chat
- @tanstack/query-example-react-default-query-function
- @tanstack/query-example-react-devtools-panel
- @tanstack/query-example-eslint-legacy
- @tanstack/query-example-react-infinite-query-with-max-pages
- @tanstack/query-example-react-load-more-infinite-scroll
- @tanstack/query-example-react-nextjs
- @tanstack/query-example-react-nextjs-app-prefetching
- @tanstack/query-example-nextjs-suspense-streaming
- @tanstack/query-example-react-offline
- @tanstack/query-example-react-optimistic-updates-cache
- @tanstack/query-example-react-optimistic-updates-ui
- @tanstack/query-example-react-pagination
- @tanstack/query-example-react-playground
- @tanstack/query-example-react-prefetching
- @tanstack/query-example-react-react-native
- @tanstack/query-example-react-router
- @tanstack/query-example-react-rick-morty
- @tanstack/query-example-react-shadow-dom
- @tanstack/query-example-react-simple
- @tanstack/query-example-react-star-wars
- @tanstack/query-example-react-suspense
- @tanstack/query-example-svelte-auto-refetching
- @tanstack/query-example-svelte-basic
- @tanstack/query-example-svelte-load-more-infinite-scroll
- @tanstack/query-example-svelte-optimistic-updates
- @tanstack/query-example-svelte-playground
- @tanstack/query-example-svelte-simple
- @tanstack/query-example-svelte-ssr
- @tanstack/query-example-svelte-star-wars
- @tanstack/query-example-vue-2.6-basic
- @tanstack/query-example-vue-2.7-basic
- @tanstack/query-example-vue-basic
- @tanstack/query-example-vue-dependent-queries
- @tanstack/query-example-vue-nuxt3
- @tanstack/query-example-vue-persister
- @tanstack/query-example-vue-simple
@tanstack/angular-query-experimental
npm i https://pkg.pr.new/@tanstack/angular-query-experimental@9653
@tanstack/eslint-plugin-query
npm i https://pkg.pr.new/@tanstack/eslint-plugin-query@9653
@tanstack/query-async-storage-persister
npm i https://pkg.pr.new/@tanstack/query-async-storage-persister@9653
@tanstack/query-broadcast-client-experimental
npm i https://pkg.pr.new/@tanstack/query-broadcast-client-experimental@9653
@tanstack/query-core
npm i https://pkg.pr.new/@tanstack/query-core@9653
@tanstack/query-devtools
npm i https://pkg.pr.new/@tanstack/query-devtools@9653
@tanstack/query-persist-client-core
npm i https://pkg.pr.new/@tanstack/query-persist-client-core@9653
@tanstack/query-sync-storage-persister
npm i https://pkg.pr.new/@tanstack/query-sync-storage-persister@9653
@tanstack/react-query
npm i https://pkg.pr.new/@tanstack/react-query@9653
@tanstack/react-query-devtools
npm i https://pkg.pr.new/@tanstack/react-query-devtools@9653
@tanstack/react-query-next-experimental
npm i https://pkg.pr.new/@tanstack/react-query-next-experimental@9653
@tanstack/react-query-persist-client
npm i https://pkg.pr.new/@tanstack/react-query-persist-client@9653
@tanstack/solid-query
npm i https://pkg.pr.new/@tanstack/solid-query@9653
@tanstack/solid-query-devtools
npm i https://pkg.pr.new/@tanstack/solid-query-devtools@9653
@tanstack/solid-query-persist-client
npm i https://pkg.pr.new/@tanstack/solid-query-persist-client@9653
@tanstack/svelte-query
npm i https://pkg.pr.new/@tanstack/svelte-query@9653
@tanstack/svelte-query-devtools
npm i https://pkg.pr.new/@tanstack/svelte-query-devtools@9653
@tanstack/svelte-query-persist-client
npm i https://pkg.pr.new/@tanstack/svelte-query-persist-client@9653
@tanstack/vue-query
npm i https://pkg.pr.new/@tanstack/vue-query@9653
@tanstack/vue-query-devtools
npm i https://pkg.pr.new/@tanstack/vue-query-devtools@9653
commit: 29a7076
Codecov Report
:white_check_mark: All modified and coverable lines are covered by tests.
:white_check_mark: Project coverage is 94.13%. Comparing base (7d370b9) to head (29a7076).
:warning: Report is 3 commits behind head on main.
Additional details and impacted files
@@ Coverage Diff @@
## main #9653 +/- ##
===========================================
+ Coverage 46.19% 94.13% +47.93%
===========================================
Files 213 21 -192
Lines 8453 426 -8027
Branches 1909 99 -1810
===========================================
- Hits 3905 401 -3504
+ Misses 4105 24 -4081
+ Partials 443 1 -442
| Components | Coverage Δ | |
|---|---|---|
| @tanstack/angular-query-experimental | 93.85% <ø> (-0.23%) |
:arrow_down: |
| @tanstack/eslint-plugin-query | ∅ <ø> (∅) |
|
| @tanstack/query-async-storage-persister | ∅ <ø> (∅) |
|
| @tanstack/query-broadcast-client-experimental | ∅ <ø> (∅) |
|
| @tanstack/query-codemods | ∅ <ø> (∅) |
|
| @tanstack/query-core | ∅ <ø> (∅) |
|
| @tanstack/query-devtools | ∅ <ø> (∅) |
|
| @tanstack/query-persist-client-core | ∅ <ø> (∅) |
|
| @tanstack/query-sync-storage-persister | ∅ <ø> (∅) |
|
| @tanstack/query-test-utils | ∅ <ø> (∅) |
|
| @tanstack/react-query | ∅ <ø> (∅) |
|
| @tanstack/react-query-devtools | ∅ <ø> (∅) |
|
| @tanstack/react-query-next-experimental | ∅ <ø> (∅) |
|
| @tanstack/react-query-persist-client | ∅ <ø> (∅) |
|
| @tanstack/solid-query | ∅ <ø> (∅) |
|
| @tanstack/solid-query-devtools | ∅ <ø> (∅) |
|
| @tanstack/solid-query-persist-client | ∅ <ø> (∅) |
|
| @tanstack/svelte-query | ∅ <ø> (∅) |
|
| @tanstack/svelte-query-devtools | ∅ <ø> (∅) |
|
| @tanstack/svelte-query-persist-client | ∅ <ø> (∅) |
|
| @tanstack/vue-query | ∅ <ø> (∅) |
|
| @tanstack/vue-query-devtools | ∅ <ø> (∅) |
:rocket: New features to boost your workflow:
- :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
- :package: JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
⚠️ No Changeset found
Latest commit: ecb7fd3d4447f23700d6078310e323b825278ef6
Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.
This PR includes no changesets
When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR