From 80dd43b2f6d06ac580ced07eb93cd45f39393d09 Mon Sep 17 00:00:00 2001 From: Wonsuk Choi Date: Sun, 6 Sep 2026 23:59:40 +0900 Subject: [PATCH 1/2] fix(angular-query-experimental): widen 'SkipToken' to 'symbol' so it type-checks inside a whole-options getter --- ...-injectquery-skiptoken-getter-typecheck.md | 5 +++ .../src/__tests__/inject-query.test-d.ts | 16 ++++++- .../src/__tests__/inject-query.test.ts | 45 +++++++++++++++++++ .../src/__tests__/query-options.test-d.ts | 10 +++++ .../src/inject-query.ts | 6 ++- .../src/query-options.ts | 13 +++--- .../angular-query-experimental/src/types.ts | 16 +++++-- 7 files changed, 99 insertions(+), 12 deletions(-) create mode 100644 .changeset/angular-query-injectquery-skiptoken-getter-typecheck.md diff --git a/.changeset/angular-query-injectquery-skiptoken-getter-typecheck.md b/.changeset/angular-query-injectquery-skiptoken-getter-typecheck.md new file mode 100644 index 00000000000..c4e64a4c802 --- /dev/null +++ b/.changeset/angular-query-injectquery-skiptoken-getter-typecheck.md @@ -0,0 +1,5 @@ +--- +'@tanstack/angular-query-experimental': patch +--- + +fix(angular-query-experimental): widen 'SkipToken' to 'symbol' so it type-checks inside a whole-options getter diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts index c0864c0c99e..46b9d0dc58b 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts @@ -1,6 +1,6 @@ import { describe, expectTypeOf, it } from 'vitest' import { queryKey, sleep } from '@tanstack/query-test-utils' -import { injectQuery, queryOptions } from '..' +import { injectQuery, queryOptions, skipToken } from '..' import type { Signal } from '@angular/core' describe('injectQuery', () => { @@ -191,4 +191,18 @@ describe('injectQuery', () => { } }) }) + + describe('skipToken', () => { + it('should narrow data to string | undefined for a conditional skipToken inside a whole-options getter', () => { + const key = queryKey() + const postId: number | undefined = 1 + const query = injectQuery(() => ({ + queryKey: [...key, postId], + queryFn: + postId != null ? () => sleep(0).then(() => `post ${postId}`) : skipToken, + })) + + expectTypeOf(query.data).toEqualTypeOf>() + }) + }) }) diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts index af3257f3bbe..f1b55f6c9fe 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test.ts @@ -32,6 +32,7 @@ import { injectQuery, provideIsRestoring, provideTanStackQuery, + skipToken, } from '..' import { setSignalInputs } from './test-utils' import type { CreateQueryOptions, OmitKeyof, QueryFunction } from '..' @@ -920,4 +921,48 @@ describe('injectQuery', () => { expect(callCount).toBe(2) }) }) + + describe('skipToken', () => { + it('should not fetch when queryFn is skipToken, and fetch once it is replaced', async () => { + const key = queryKey() + const queryFn = vi.fn(() => sleep(10).then(() => 'post 1')) + + @Component({ + template: ` +
status: {{ query.status() }}
+
isFetching: {{ query.isFetching() }}
+
data: {{ query.data() ?? 'none' }}
+ `, + }) + class Page { + postId = signal(undefined) + + readonly query = injectQuery(() => ({ + queryKey: [...key, this.postId()], + queryFn: this.postId() != null ? queryFn : skipToken, + })) + } + + const rendered = await render(Page) + + expect(rendered.getByText('status: pending')).toBeInTheDocument() + expect(rendered.getByText('isFetching: false')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(11) + rendered.fixture.detectChanges() + expect(queryFn).not.toHaveBeenCalled() + expect(rendered.getByText('status: pending')).toBeInTheDocument() + expect(rendered.getByText('isFetching: false')).toBeInTheDocument() + + rendered.fixture.componentInstance.postId.set('1') + rendered.fixture.detectChanges() + expect(rendered.getByText('isFetching: true')).toBeInTheDocument() + + await vi.advanceTimersByTimeAsync(11) + rendered.fixture.detectChanges() + expect(queryFn).toHaveBeenCalledTimes(1) + expect(rendered.getByText('status: success')).toBeInTheDocument() + expect(rendered.getByText('data: post 1')).toBeInTheDocument() + }) + }) }) diff --git a/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts b/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts index c4462afaa34..f41c16b5974 100644 --- a/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts @@ -8,6 +8,7 @@ import { skipToken, } from '..' import type { Signal } from '@angular/core' +import type { UnusedSkipTokenOptions } from '..' // Regression test for exported queryOptions inference under declaration emit. // TypeScript should be able to name the return type without expanding the @@ -122,6 +123,15 @@ it('should work when passed to query with skipToken', () => { assertType>(data) }) +it('should not allow skipToken on UnusedSkipTokenOptions', () => { + const options: UnusedSkipTokenOptions> = { + queryKey: ['key'], + // @ts-expect-error skipToken should not be assignable here + queryFn: skipToken, + } + expectTypeOf(options.queryKey).toEqualTypeOf>() +}) + it('should tag the queryKey with the result type of the QueryFn', () => { const key = queryKey() const { queryKey: tagged } = queryOptions({ diff --git a/packages/angular-query-experimental/src/inject-query.ts b/packages/angular-query-experimental/src/inject-query.ts index 973538ca053..c3670765eec 100644 --- a/packages/angular-query-experimental/src/inject-query.ts +++ b/packages/angular-query-experimental/src/inject-query.ts @@ -8,6 +8,7 @@ import { import { createBaseQuery } from './create-base-query' import type { DefaultError, QueryKey } from '@tanstack/query-core' import type { + CreateBaseQueryOptions, CreateQueryOptions, CreateQueryResult, DefinedCreateQueryResult, @@ -203,6 +204,9 @@ export function injectQuery( ) { !options?.injector && assertInInjectionContext(injectQuery) return runInInjectionContext(options?.injector ?? inject(Injector), () => - createBaseQuery(injectQueryFn, QueryObserver), + createBaseQuery( + injectQueryFn as () => CreateBaseQueryOptions, + QueryObserver, + ), ) as unknown as CreateQueryResult } diff --git a/packages/angular-query-experimental/src/query-options.ts b/packages/angular-query-experimental/src/query-options.ts index 0de61895e72..9ade14072a2 100644 --- a/packages/angular-query-experimental/src/query-options.ts +++ b/packages/angular-query-experimental/src/query-options.ts @@ -62,10 +62,7 @@ export type UnusedSkipTokenOptions< * fetch that fails with "Missing queryFn" unless `enabled` is `false` or a default query function has been * defined. A default query function only supplies `queryFn`; it doesn't defer the fetch on its own. */ - queryFn?: Exclude< - CreateQueryOptions['queryFn'], - SkipToken | undefined - > + queryFn?: QueryFunction } /** @@ -285,8 +282,12 @@ export function queryOptions< TQueryKey extends QueryKey = QueryKey, >( options: UndefinedInitialDataOptions, -): UndefinedInitialDataOptions & - QueryKeyWithDataTag +): OmitKeyof< + UndefinedInitialDataOptions, + 'queryFn' +> & { + queryFn?: QueryFunction | SkipToken +} & QueryKeyWithDataTag export function queryOptions(options: unknown) { return options diff --git a/packages/angular-query-experimental/src/types.ts b/packages/angular-query-experimental/src/types.ts index b0acd8579fc..9a0d3040c8d 100644 --- a/packages/angular-query-experimental/src/types.ts +++ b/packages/angular-query-experimental/src/types.ts @@ -11,6 +11,7 @@ import type { MutationObserverResult, OmitKeyof, Override, + QueryFunction, QueryKey, QueryObserverOptions, QueryObserverResult, @@ -18,6 +19,9 @@ import type { import type { Signal } from '@angular/core' import type { MapToSignals } from './signal-proxy' +// Widen the type of the symbol to enable type inference even if skipToken is not immutable. +type SkipTokenForCreateQueryOptions = symbol + /** * The options shared across `angular-query-experimental`'s query functions. Extends * {@link QueryObserverOptions} from `@tanstack/query-core` as-is — unlike `react-query`, @@ -55,15 +59,19 @@ export interface CreateBaseQueryOptions< * `select` is used. * @template TQueryKey - The type of your `queryKey`. */ -export interface CreateQueryOptions< +export type CreateQueryOptions< TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, -> extends OmitKeyof< +> = OmitKeyof< CreateBaseQueryOptions, - 'suspense' -> {} + 'suspense' | 'queryFn' +> & { + queryFn?: + | QueryFunction + | SkipTokenForCreateQueryOptions +} type CreateStatusBasedQueryResult< TStatus extends QueryObserverResult['status'], From f77a39f4277df4f6a97f49ba73d25be152ca8200 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:01:27 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .../src/__tests__/inject-query.test-d.ts | 4 +++- .../src/__tests__/query-options.test-d.ts | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts index 46b9d0dc58b..df8a54f5c1d 100644 --- a/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/inject-query.test-d.ts @@ -199,7 +199,9 @@ describe('injectQuery', () => { const query = injectQuery(() => ({ queryKey: [...key, postId], queryFn: - postId != null ? () => sleep(0).then(() => `post ${postId}`) : skipToken, + postId != null + ? () => sleep(0).then(() => `post ${postId}`) + : skipToken, })) expectTypeOf(query.data).toEqualTypeOf>() diff --git a/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts b/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts index f41c16b5974..15fe9f0f0bf 100644 --- a/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts +++ b/packages/angular-query-experimental/src/__tests__/query-options.test-d.ts @@ -124,7 +124,12 @@ it('should work when passed to query with skipToken', () => { }) it('should not allow skipToken on UnusedSkipTokenOptions', () => { - const options: UnusedSkipTokenOptions> = { + const options: UnusedSkipTokenOptions< + number, + Error, + number, + Array + > = { queryKey: ['key'], // @ts-expect-error skipToken should not be assignable here queryFn: skipToken,