Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -191,4 +191,20 @@ 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<Signal<string | undefined>>()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
injectQuery,
provideIsRestoring,
provideTanStackQuery,
skipToken,
} from '..'
import { setSignalInputs } from './test-utils'
import type { CreateQueryOptions, OmitKeyof, QueryFunction } from '..'
Expand Down Expand Up @@ -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: `
<div>status: {{ query.status() }}</div>
<div>isFetching: {{ query.isFetching() }}</div>
<div>data: {{ query.data() ?? 'none' }}</div>
`,
})
class Page {
postId = signal<string | undefined>(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()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,6 +123,20 @@ it('should work when passed to query with skipToken', () => {
assertType<Promise<unknown>>(data)
})

it('should not allow skipToken on UnusedSkipTokenOptions', () => {
const options: UnusedSkipTokenOptions<
number,
Error,
number,
Array<string>
> = {
queryKey: ['key'],
// @ts-expect-error skipToken should not be assignable here
queryFn: skipToken,
}
expectTypeOf(options.queryKey).toEqualTypeOf<Array<string>>()
})

it('should tag the queryKey with the result type of the QueryFn', () => {
const key = queryKey()
const { queryKey: tagged } = queryOptions({
Expand Down
6 changes: 5 additions & 1 deletion packages/angular-query-experimental/src/inject-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
13 changes: 7 additions & 6 deletions packages/angular-query-experimental/src/query-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TQueryFnData, TError, TData, TQueryKey>['queryFn'],
SkipToken | undefined
>
queryFn?: QueryFunction<TQueryFnData, TQueryKey>
}

/**
Expand Down Expand Up @@ -285,8 +282,12 @@ export function queryOptions<
TQueryKey extends QueryKey = QueryKey,
>(
options: UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
): UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey> &
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
): OmitKeyof<
UndefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryFn'
> & {
queryFn?: QueryFunction<TQueryFnData, TQueryKey> | SkipToken
} & QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>

export function queryOptions(options: unknown) {
return options
Expand Down
16 changes: 12 additions & 4 deletions packages/angular-query-experimental/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,17 @@ import type {
MutationObserverResult,
OmitKeyof,
Override,
QueryFunction,
QueryKey,
QueryObserverOptions,
QueryObserverResult,
} from '@tanstack/query-core'
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`,
Expand Down Expand Up @@ -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<TQueryFnData, TError, TData, TQueryFnData, TQueryKey>,
'suspense'
> {}
'suspense' | 'queryFn'
> & {
queryFn?:
| QueryFunction<TQueryFnData, TQueryKey>
| SkipTokenForCreateQueryOptions
}

type CreateStatusBasedQueryResult<
TStatus extends QueryObserverResult['status'],
Expand Down
Loading