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
5 changes: 5 additions & 0 deletions .changeset/vue-query-skiptoken-getter-typecheck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/vue-query': patch
---

fix(vue-query): widen 'SkipToken' to 'symbol' so it type-checks inside a whole-options getter
35 changes: 34 additions & 1 deletion packages/vue-query/src/__tests__/useQuery.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { assertType, describe, expectTypeOf, it } from 'vitest'
import { computed, reactive, ref } from 'vue-demi'
import { queryKey, sleep } from '@tanstack/query-test-utils'
import { queryOptions, useQuery } from '..'
import { queryOptions, skipToken, useQuery } from '..'
import type { Ref } from 'vue-demi'
import type { OmitKeyof, UseQueryOptions, UseQueryReturnType } from '..'

Expand Down Expand Up @@ -385,4 +385,37 @@ describe('useQuery', () => {
expectTypeOf(data.value).toEqualTypeOf<number | undefined>()
})
})

describe('skipToken', () => {
it('should narrow data to string | undefined for a conditional skipToken inside a whole-options getter', () => {
const postId = ref<number>()

const { data } = useQuery(() => {
const id = postId.value
return {
queryKey: ['post', id],
queryFn:
id != null ? () => sleep(0).then(() => `post ${id}`) : skipToken,
}
})

expectTypeOf(data.value).toEqualTypeOf<string | undefined>()
})

it('known tradeoff: widening SkipToken to a plain symbol also accepts unrelated symbol values', () => {
// `queryFn`'s type accepts any `symbol`, not just `SkipToken`, because narrowing to the `unique
// symbol` that `SkipToken` actually is breaks type inference for the ternary above β€” same tradeoff
// already accepted in `useQueries.ts`'s `SkipTokenForUseQueries`. This isn't type-safe, but the
// runtime only ever compares `options.queryFn === skipToken` by identity, so passing an unrelated
// symbol here just behaves like `skipToken` at runtime too.
const unrelatedSymbol: unique symbol = Symbol('unrelated')

const { data } = useQuery({
queryKey: ['post'],
queryFn: unrelatedSymbol,
})

expectTypeOf(data.value).toEqualTypeOf<unknown>()
})
})
})
56 changes: 45 additions & 11 deletions packages/vue-query/src/queryOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@ import type {
DefaultError,
InitialDataFunction,
NonUndefinedGuard,
OmitKeyof,
QueryBooleanOption,
QueryFunction,
QueryKey,
QueryKeyWithDataTag,
QueryObserverOptions,
} from '@tanstack/query-core'

// Widen `SkipToken`'s `unique symbol` to `symbol` so it survives a `queryFn: cond ? fn : skipToken`
// ternary inside a whole-options getter β€” see `SkipTokenForUseQueries` in `useQueries.ts`.
type SkipTokenForQueryOptions = symbol

/**
* The options accepted by `queryOptions`, `useQuery`, and the other query hooks. `enabled` tracks reactive
* dependencies automatically as a `ref`, a plain value, or a reactive getter (`() => ...`). `queryKey` reacts
Expand Down Expand Up @@ -56,13 +62,18 @@ export type QueryOptions<
>)
: Property extends 'queryKey'
? MaybeRefOrGetter<TQueryKey>
: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
DeepUnwrapRef<TQueryKey>
>[Property]
: Property extends 'queryFn'
?
| QueryFunction<TQueryFnData, DeepUnwrapRef<TQueryKey>>
| SkipTokenForQueryOptions
| undefined
: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
DeepUnwrapRef<TQueryKey>
>[Property]
} & ShallowOption

/**
Expand Down Expand Up @@ -120,21 +131,44 @@ export type DefinedInitialQueryOptions<
| (() => NonUndefinedGuard<TQueryFnData>)
}

// `UndefinedInitialQueryOptions`/`DefinedInitialQueryOptions` widen `queryFn` to plain `symbol` so a
// `queryFn: cond ? fn : skipToken` ternary type-checks as a getter's *input*. Narrow it back to `SkipToken`
// here so the *returned* options object still satisfies `QueryClient` methods that expect `unique symbol`.
export type UndefinedInitialQueryOptionsWithDataTag<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey> &
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
> = OmitKeyof<
UndefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryFn'
> & {
queryFn?: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
DeepUnwrapRef<TQueryKey>
>['queryFn']
} & QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>

export type DefinedInitialQueryOptionsWithDataTag<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> = DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey> &
QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>
> = OmitKeyof<
DefinedInitialQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryFn'
> & {
queryFn?: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryFnData,
DeepUnwrapRef<TQueryKey>
>['queryFn']
} & QueryKeyWithDataTag<TQueryKey, TQueryFnData, TError>

/**
* You can generally pass everything to `queryOptions` that you can also pass to `useQuery`. These options can
Expand Down
29 changes: 20 additions & 9 deletions packages/vue-query/src/useQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
InitialDataFunction,
NonUndefinedGuard,
QueryBooleanOption,
QueryFunction,
QueryKey,
QueryObserverOptions,
} from '@tanstack/query-core'
Expand All @@ -19,6 +20,10 @@ import type {
} from './types'
import type { QueryClient } from './queryClient'

// Widen `SkipToken`'s `unique symbol` to `symbol` so it survives a `queryFn: cond ? fn : skipToken`
// ternary inside a whole-options getter β€” see `SkipTokenForUseQueries` in `useQueries.ts`.
type SkipTokenForUseQuery = symbol

export type UseQueryOptions<
TQueryFnData = unknown,
TError = DefaultError,
Expand Down Expand Up @@ -52,15 +57,21 @@ export type UseQueryOptions<
TQueryKey
>[Property]
>
: MaybeRefDeep<
QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
DeepUnwrapRef<TQueryKey>
>[Property]
>
: Property extends 'queryFn'
? MaybeRefDeep<
| QueryFunction<TQueryFnData, DeepUnwrapRef<TQueryKey>>
| SkipTokenForUseQuery
| undefined
>
: MaybeRefDeep<
QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
DeepUnwrapRef<TQueryKey>
>[Property]
>
} & ShallowOption
>

Expand Down
Loading