Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Include optional TError on DataTag so that getQueryState can infer the error type correctly #8361

Merged
merged 11 commits into from
Nov 29, 2024
30 changes: 30 additions & 0 deletions packages/query-core/src/__tests__/queryClient.test-d.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '../queryClient'
import type { QueryState } from '../query'
import type { DataTag, InfiniteData, QueryKey } from '../types'

describe('getQueryData', () => {
Expand Down Expand Up @@ -101,6 +102,35 @@ describe('setQueryData', () => {
})
})

describe('getQueryState', () => {
it('should be loose typed without tag', () => {
const queryKey = ['key'] as const
const queryClient = new QueryClient()
const data = queryClient.getQueryState(queryKey)

expectTypeOf(data).toEqualTypeOf<QueryState<unknown, Error> | undefined>()
})

it('should be typed if key is tagged', () => {
const queryKey = ['key'] as DataTag<Array<string>, number>
const queryClient = new QueryClient()
const data = queryClient.getQueryState(queryKey)

expectTypeOf(data).toEqualTypeOf<QueryState<number, Error> | undefined>()
})

it('should be typed including error if key is tagged', () => {
type CustomError = Error & { customError: string }
const queryKey = ['key'] as DataTag<Array<string>, number, CustomError>
const queryClient = new QueryClient()
const data = queryClient.getQueryState(queryKey)

expectTypeOf(data).toEqualTypeOf<
QueryState<number, CustomError> | undefined
>()
})
})

describe('fetchInfiniteQuery', () => {
it('should allow passing pages', async () => {
const data = await new QueryClient().fetchInfiniteQuery({
Expand Down
26 changes: 23 additions & 3 deletions packages/query-core/src/__tests__/utils.test-d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ import type { DataTag } from '../types'
describe('QueryFilters', () => {
it('should be typed if generics are passed', () => {
type TData = { a: number; b: string }
type TError = { message: string }

const a: QueryFilters<TData, TError> = {
const a: QueryFilters<TData> = {
predicate(query) {
expectTypeOf(query.setData({ a: 1, b: '1' })).toEqualTypeOf<TData>()
return true
Expand All @@ -22,7 +21,28 @@ describe('QueryFilters', () => {
expectTypeOf(data).toEqualTypeOf<TData | undefined>()

const error = queryClient.getQueryState(a.queryKey!)?.error
expectTypeOf(error).toEqualTypeOf<Error | null | undefined>() // maybe one day this can return TError
expectTypeOf(error).toEqualTypeOf<Error | null | undefined>()
})

it('should be typed if generics are passed including an error type', () => {
type TData = { a: number; b: string }
type TError = Error & { message: string }

const a: QueryFilters<TData, TError> = {
predicate(query) {
expectTypeOf(query.setData({ a: 1, b: '1' })).toEqualTypeOf<TData>()
return true
},
queryKey: ['key'] as DataTag<undefined, TData, TError>,
}

const queryClient = new QueryClient()

const data = queryClient.getQueryData(a.queryKey!)
expectTypeOf(data).toEqualTypeOf<TData | undefined>()

const error = queryClient.getQueryState(a.queryKey!)?.error
expectTypeOf(error).toEqualTypeOf<TError | null | undefined>()
})

it('should be loose typed if generics are defaults', () => {
Expand Down
16 changes: 13 additions & 3 deletions packages/query-core/src/queryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,12 +229,22 @@ export class QueryClient {
>
? TaggedValue
: TQueryFnData,
TInferredError = TTaggedQueryKey extends DataTag<
unknown,
unknown,
infer TaggedError
>
? unknown extends TaggedError
? TError
: TaggedError
: TError,
TkDodo marked this conversation as resolved.
Show resolved Hide resolved
>(
queryKey: TTaggedQueryKey,
): QueryState<TInferredQueryFnData, TError> | undefined {
): QueryState<TInferredQueryFnData, TInferredError> | undefined {
const options = this.defaultQueryOptions({ queryKey })
return this.#queryCache.get<TInferredQueryFnData, TError>(options.queryHash)
?.state
return this.#queryCache.get<TInferredQueryFnData, TInferredError>(
options.queryHash,
)?.state
}

removeQueries(filters?: QueryFilters): void {
Expand Down
4 changes: 3 additions & 1 deletion packages/query-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ export type DefaultError = Register extends {
export type QueryKey = ReadonlyArray<unknown>

export declare const dataTagSymbol: unique symbol
export type DataTag<TType, TValue> = TType & {
export declare const dataTagErrorSymbol: unique symbol
export type DataTag<TType, TValue, TError = unknown> = TType & {
TkDodo marked this conversation as resolved.
Show resolved Hide resolved
[dataTagSymbol]: TValue
[dataTagErrorSymbol]: TError
}

export type QueryFunction<
Expand Down
2 changes: 1 addition & 1 deletion packages/query-core/src/utils.ts
TkDodo marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface QueryFilters<
*/
queryKey?: unknown extends TQueryFnData
? QueryKey
: QueryKey & DataTag<unknown, TQueryFnData>
: QueryKey & DataTag<unknown, TQueryFnData, TError>
/**
* Include or exclude stale queries
*/
Expand Down