Skip to content

Conversation

mdm317
Copy link

@mdm317 mdm317 commented Sep 14, 2025


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 Omit

    Currently, BaseQueryNarrowing only works in injectQuery when initialData is 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.

Copy link
Contributor

coderabbitai bot commented Sep 14, 2025

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 update
packages/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 implementations
packages/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 tests
packages/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 change
examples/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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

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.

❤️ Share

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

@mdm317 mdm317 changed the title fix: narrow injectInfiniteQuery type with initial data fix(angular-query): fix injectInfiniteQuery to narrow type Sep 14, 2025
Copy link
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

🧹 Nitpick comments (3)
packages/angular-query-experimental/src/types.ts (1)

160-161: Style: spacing/formatting nits

Missing space after = and inconsistent indentation compared to neighboring aliases. Run the formatter or align manually (also covered by the diff above).

packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts (2)

26-77: Good coverage for narrowing without initialData

The three cases correctly validate narrowing behavior: data undefined on isPending, data defined on isSuccess, and error defined on isError.

Consider adding one negative assertion outside guards to ensure data() is InfiniteData<string, unknown> | undefined by default (no initialData).


79-141: Excellent: with initialData, pending still yields defined data

These tests capture the critical regression vector: isPending should still expose defined data() when initialData is present. The added isSuccess/isError checks round it out nicely.

Optionally assert that data() remains defined in the isError branch with initialData, mirroring runtime behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6492aed and e3b11c0.

📒 Files selected for processing (2)
  • packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts (1 hunks)
  • packages/angular-query-experimental/src/types.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/angular-query-experimental/src/__tests__/inject-infinite-query.test-d.ts (1)
packages/query-core/src/types.ts (1)
  • InfiniteData (204-207)
packages/angular-query-experimental/src/types.ts (1)
packages/angular-query-experimental/src/signal-proxy.ts (1)
  • MapToSignals (4-6)
🔇 Additional comments (1)
packages/angular-query-experimental/src/types.ts (1)

160-161: Sanity-check complete — omission of BaseQueryNarrowing flags is correct
packages/query-core/src/types.ts defines isError/isPending/isSuccess; packages/angular-query-experimental/src/types.ts uses MapToSignals<OmitKeyof<TState, keyof BaseQueryNarrowing, 'safely'>> to drop those keys — no change required.

@mdm317 mdm317 marked this pull request as draft September 14, 2025 08:08
Copy link

nx-cloud bot commented Sep 14, 2025

View your CI Pipeline Execution ↗ for commit 29a7076

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

InfiniteQueryObserver as typeof QueryObserver,
),
)
) as unknown as CreateInfiniteQueryResult
Copy link
Author

Choose a reason for hiding this comment

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

the type assertion is necessary for ts 5.4 and lower

return runInInjectionContext(options?.injector ?? inject(Injector), () =>
createBaseQuery(injectQueryFn, QueryObserver),
) as unknown as CreateQueryResult
) as unknown as CreateQueryResult | DefinedCreateQueryResult
Copy link
Author

Choose a reason for hiding this comment

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

the type assertion is necessary for ts 5.4 and lower

@mdm317 mdm317 marked this pull request as ready for review September 20, 2025 07:52
@arnoud-dv arnoud-dv self-assigned this Sep 20, 2025
Copy link

pkg-pr-new bot commented Sep 20, 2025

More templates

@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

Copy link
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: 0

🧹 Nitpick comments (2)
packages/angular-query-experimental/src/inject-query.ts (1)

225-225: Consider documenting the union return type for clarity.

The widened return type to CreateQueryResult | DefinedCreateQueryResult correctly supports both query result variants. Similar to inject-infinite-query.ts, consider adding a comment explaining the type cast requirement.

-  ) as unknown as CreateQueryResult | DefinedCreateQueryResult
+  // Union return type supports both defined and undefined initial data scenarios
+  // Double cast through unknown required for TypeScript 5.4 and lower compatibility
+  ) as unknown as CreateQueryResult | DefinedCreateQueryResult
packages/angular-query-experimental/src/inject-infinite-query.ts (1)

124-124: Document the double-cast for TypeScript ≤ 5.4 compatibility

Add this comment immediately above the double-cast in both files:

// Double cast through unknown required for TypeScript 5.4 and lower compatibility

Locations:

  • packages/angular-query-experimental/src/inject-query.ts — at the return cast to CreateQueryResult | DefinedCreateQueryResult
  • packages/angular-query-experimental/src/inject-infinite-query.ts — at the return cast to CreateInfiniteQueryResult
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3b11c0 and 29a7076.

📒 Files selected for processing (5)
  • examples/angular/infinite-query-with-max-pages/src/app/components/example.component.html (1 hunks)
  • packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts (1 hunks)
  • packages/angular-query-experimental/src/inject-infinite-query.ts (1 hunks)
  • packages/angular-query-experimental/src/inject-query.ts (1 hunks)
  • packages/angular-query-experimental/src/types.ts (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
packages/angular-query-experimental/src/inject-infinite-query.ts (1)
packages/angular-query-experimental/src/types.ts (1)
  • CreateInfiniteQueryResult (133-138)
packages/angular-query-experimental/src/types.ts (2)
packages/query-core/src/types.ts (5)
  • QueryObserverResult (899-904)
  • DefaultError (47-51)
  • OmitKeyof (19-29)
  • InfiniteQueryObserverResult (1060-1068)
  • DefinedInfiniteQueryObserverResult (1053-1058)
packages/angular-query-experimental/src/signal-proxy.ts (1)
  • MapToSignals (4-6)
🔇 Additional comments (8)
packages/angular-query-experimental/src/types.ts (6)

51-56: LGTM! Correct approach for type narrowing support.

Adding the generic TState parameter to CreateStatusBasedQueryResult enables proper type narrowing for both regular queries and infinite queries by allowing union type distribution.


58-65: LGTM! Well-structured narrowed result type.

The new CreateNarrowQueryResult type appropriately combines BaseQueryNarrowing with the narrowed state signals, using OmitKeyof with 'safely' to prevent key collisions between the function guards and signal properties.


66-95: LGTM! Comprehensive type narrowing implementation.

The updated BaseQueryNarrowing interface correctly implements the type narrowing pattern by:

  1. Making it generic over TState to work with different query result types
  2. Using CreateNarrowQueryResult with narrowed state in the type guards
  3. Properly extracting status-specific types with CreateStatusBasedQueryResult

This enables the desired behavior where checking isSuccess() narrows the data type to non-undefined.


114-120: LGTM! Consistent update to base query result type.

The addition of the TState parameter and updated type composition aligns CreateBaseQueryResult with the new narrowing system while maintaining backward compatibility through the default parameter.


126-132: LGTM! Proper handling of defined query results.

The DefinedCreateQueryResult type correctly uses DefinedQueryObserverResult as the default state, ensuring type safety for queries with guaranteed data.


133-146: LGTM! Infinite query types properly support narrowing.

Both CreateInfiniteQueryResult and DefinedCreateInfiniteQueryResult are correctly updated with:

  • Appropriate TState parameters defaulting to their respective observer result types
  • Consistent use of BaseQueryNarrowing and MapToSignals with OmitKeyof
  • Proper resolution of the key collision issue mentioned in past reviews
packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts (1)

108-117: LGTM! Comprehensive test for error state narrowing.

The test correctly verifies that when isError() returns true, the error property is properly typed as Signal<Error> even when initialData is provided. This confirms the type narrowing works as expected.

examples/angular/infinite-query-with-max-pages/src/app/components/example.component.html (1)

5-11: LGTM! Template structure aligns with enhanced type guards.

The change from nested if-else to independent @if blocks is a good improvement that:

  • Makes each state check independent and more maintainable
  • Aligns well with the type narrowing implementation where each guard (isPending, isError, isSuccess) provides specific type information
  • Improves readability by clearly separating the three distinct states

Copy link

codecov bot commented Sep 21, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.13%. Comparing base (7d370b9) to head (29a7076).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@             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%) ⬇️
@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 ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link

changeset-bot bot commented Sep 26, 2025

⚠️ No Changeset found

Latest commit: ecb7fd3

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

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.

Angular: no type narrowing on infinite query status
3 participants