Skip to content

Commit

Permalink
fix(types): Fix type errors when using fully-typed inputs like QueryF…
Browse files Browse the repository at this point in the history
…ilters<Data, Error>, and test all QueryClient methods to detect similar issues (#8375)

* Add test with failing types

* Tidy up

* Move tests to correct file

* Add initial fixes (some review and testing needed)

* Formatting

* Ensure T params are mandatory for new overloads

* Fix type issues

* Unnecessary DataTag

* Add typeless tests

* Fix some cases

* Fixing more cases

* Fix some typings

* Fix remaining issues

* Fix linting errors

* Fix one warning

* Revert unnecessary change to getQueryDefaults

* Remove mutation defaults todo

* Rename var

* Remove all additional overloads in favour of a single definition which supports all cases

* Change type tests to more idomatic api
  • Loading branch information
Nick-Lucas authored Dec 3, 2024
1 parent ceb4094 commit 175757a
Show file tree
Hide file tree
Showing 4 changed files with 437 additions and 47 deletions.
321 changes: 319 additions & 2 deletions packages/query-core/src/__tests__/queryClient.test-d.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
import { describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '../queryClient'
import type { QueryState } from '../query'
import type { DataTag, InfiniteData, QueryKey } from '../types'
import type { MutationFilters, QueryFilters, Updater } from '../utils'
import type { Mutation } from '../mutation'
import type { Query, QueryState } from '../query'
import type {
DataTag,
DefaultError,
DefaultedQueryObserverOptions,
EnsureQueryDataOptions,
FetchInfiniteQueryOptions,
InfiniteData,
MutationOptions,
OmitKeyof,
QueryKey,
QueryObserverOptions,
} from '../types'

describe('getQueryData', () => {
it('should be typed if key is tagged', () => {
Expand Down Expand Up @@ -184,3 +197,307 @@ describe('defaultOptions', () => {
})
})
})

describe('fully typed usage', () => {
it('type-checks various methods with data & error included in the type', async () => {
const queryClient = new QueryClient()

type TData = { foo: string }
type TError = DefaultError & { bar: string }

//
// Construct typed arguments
//

const queryOptions: EnsureQueryDataOptions<TData, TError> = {
queryKey: ['key'] as any,
}
const fetchInfiniteQueryOptions: FetchInfiniteQueryOptions<TData, TError> =
{
queryKey: ['key'] as any,
pages: 5,
getNextPageParam: (lastPage) => {
expectTypeOf(lastPage).toEqualTypeOf<TData>()
return 0
},
initialPageParam: 0,
}
const mutationOptions: MutationOptions<TData, TError> = {}

const queryFilters: QueryFilters<
TData,
TError,
TData,
QueryKey & DataTag<unknown, TData, TError>
> = {
predicate(query) {
expectTypeOf(query).toEqualTypeOf<
Query<
TData,
TError,
TData,
QueryKey & DataTag<unknown, TData, TError>
>
>()
expectTypeOf(query.state.data).toEqualTypeOf<TData | undefined>()
expectTypeOf(query.state.error).toEqualTypeOf<TError | null>()
return false
},
}
const queryKey = queryFilters.queryKey!

const mutationFilters: MutationFilters<TData, TError> = {
predicate(mutation) {
expectTypeOf(mutation).toEqualTypeOf<Mutation<TData, TError>>()
expectTypeOf(mutation.state.data).toEqualTypeOf<TData | undefined>()
expectTypeOf(mutation.state.error).toEqualTypeOf<TError | null>()
return false
},
}
const mutationKey = mutationOptions.mutationKey!

//
// Method type tests
//

const state = queryClient.getQueryState(queryKey)
expectTypeOf(state).toEqualTypeOf<QueryState<TData, TError> | undefined>()

const queryData1 = queryClient.getQueryData(queryKey)
expectTypeOf(queryData1).toEqualTypeOf<TData | undefined>()

const queryData2 = await queryClient.ensureQueryData(queryOptions)
expectTypeOf(queryData2).toEqualTypeOf<TData>()

const queriesData = queryClient.getQueriesData(queryFilters)
expectTypeOf(queriesData).toEqualTypeOf<
Array<[QueryKey, TData | undefined]>
>()

const queryData3 = queryClient.setQueryData(queryKey, { foo: '' })
type SetQueryDataUpdaterArg = Parameters<
typeof queryClient.setQueryData<unknown, typeof queryKey>
>[1]

expectTypeOf<SetQueryDataUpdaterArg>().toEqualTypeOf<
Updater<TData | undefined, TData | undefined>
>()
expectTypeOf(queryData3).toEqualTypeOf<TData | undefined>()

const queriesData2 = queryClient.setQueriesData(queryFilters, { foo: '' }) // TODO: types here are wrong and coming up undefined
type SetQueriesDataUpdaterArg = Parameters<
typeof queryClient.setQueriesData<unknown, typeof queryFilters>
>[1]

expectTypeOf<SetQueriesDataUpdaterArg>().toEqualTypeOf<
Updater<TData | undefined, TData | undefined>
>()
expectTypeOf(queriesData2).toEqualTypeOf<
Array<[QueryKey, TData | undefined]>
>()

const queryState = queryClient.getQueryState(queryKey)
expectTypeOf(queryState).toEqualTypeOf<
QueryState<TData, TError> | undefined
>()

const fetchedQuery = await queryClient.fetchQuery(queryOptions)
expectTypeOf(fetchedQuery).toEqualTypeOf<TData>()

queryClient.prefetchQuery(queryOptions)

const infiniteQuery = await queryClient.fetchInfiniteQuery(
fetchInfiniteQueryOptions,
)
expectTypeOf(infiniteQuery).toEqualTypeOf<InfiniteData<TData, unknown>>()

const infiniteQueryData = await queryClient.ensureInfiniteQueryData(
fetchInfiniteQueryOptions,
)
expectTypeOf(infiniteQueryData).toEqualTypeOf<
InfiniteData<TData, unknown>
>()

const defaultQueryOptions = queryClient.defaultQueryOptions(queryOptions)
expectTypeOf(defaultQueryOptions).toEqualTypeOf<
DefaultedQueryObserverOptions<TData, TError, TData, TData, QueryKey>
>()

const mutationOptions2 = queryClient.defaultMutationOptions(mutationOptions)
expectTypeOf(mutationOptions2).toEqualTypeOf<
MutationOptions<TData, TError, void, unknown>
>()

queryClient.setMutationDefaults(mutationKey, {
onSettled(data, error, variables, context) {
expectTypeOf(data).toEqualTypeOf<unknown>()
expectTypeOf(error).toEqualTypeOf<DefaultError | null>()
expectTypeOf(variables).toEqualTypeOf<void>()
expectTypeOf(context).toEqualTypeOf<unknown>()
},
})

const queryDefaults = queryClient.getQueryDefaults(queryKey)
expectTypeOf(queryDefaults).toEqualTypeOf<
OmitKeyof<QueryObserverOptions<any, any, any, any, any>, 'queryKey'>
>()

// Voids and Untyped returns
queryClient.invalidateQueries(queryFilters)
queryClient.isFetching(queryFilters)
queryClient.isMutating(mutationFilters)
queryClient.removeQueries(queryFilters)
queryClient.resetQueries(queryFilters)
queryClient.cancelQueries(queryFilters)
queryClient.invalidateQueries(queryFilters)
queryClient.refetchQueries(queryFilters)
queryClient.prefetchInfiniteQuery(fetchInfiniteQueryOptions)
queryClient.setQueryDefaults(queryKey, {} as any)
queryClient.getMutationDefaults(mutationKey)
})

it('type-checks various methods with untyped arguments', async () => {
const queryClient = new QueryClient()

//
// Construct typed arguments
//

const queryOptions: EnsureQueryDataOptions = {
queryKey: ['key'] as any,
}
const fetchInfiniteQueryOptions: FetchInfiniteQueryOptions = {
queryKey: ['key'] as any,
pages: 5,
getNextPageParam: (lastPage) => {
expectTypeOf(lastPage).toEqualTypeOf<unknown>()
return 0
},
initialPageParam: 0,
}
const mutationOptions: MutationOptions = {}

const queryFilters: QueryFilters = {
predicate(query) {
expectTypeOf(query).toEqualTypeOf<Query<unknown, DefaultError>>()
expectTypeOf(query.state.data).toEqualTypeOf<unknown>()
expectTypeOf(query.state.error).toEqualTypeOf<DefaultError | null>()
return false
},
}
const queryKey = queryFilters.queryKey!

const mutationFilters: MutationFilters = {
predicate(mutation) {
expectTypeOf(mutation).toEqualTypeOf<Mutation>()
expectTypeOf(mutation.state.data).toEqualTypeOf<unknown>()
expectTypeOf(mutation.state.error).toEqualTypeOf<DefaultError | null>()
return false
},
}
const mutationKey = mutationOptions.mutationKey!

//
// Method type tests
//

const state = queryClient.getQueryState(queryKey)
expectTypeOf(state).toEqualTypeOf<
QueryState<unknown, DefaultError> | undefined
>()

const queryData1 = queryClient.getQueryData(queryKey)
expectTypeOf(queryData1).toEqualTypeOf<unknown>()

const queryData2 = await queryClient.ensureQueryData(queryOptions)
expectTypeOf(queryData2).toEqualTypeOf<unknown>()

const queriesData = queryClient.getQueriesData(queryFilters)
expectTypeOf(queriesData).toEqualTypeOf<Array<[QueryKey, unknown]>>()

const queryData3 = queryClient.setQueryData(queryKey, { foo: '' })
type SetQueryDataUpdaterArg = Parameters<
typeof queryClient.setQueryData<unknown, typeof queryKey>
>[1]

expectTypeOf<SetQueryDataUpdaterArg>().toEqualTypeOf<
Updater<unknown, unknown>
>()
expectTypeOf(queryData3).toEqualTypeOf<unknown>()

const queriesData2 = queryClient.setQueriesData(queryFilters, { foo: '' }) // TODO: types here are wrong and coming up undefined
type SetQueriesDataUpdaterArg = Parameters<
typeof queryClient.setQueriesData<unknown, typeof queryFilters>
>[1]

expectTypeOf<SetQueriesDataUpdaterArg>().toEqualTypeOf<
Updater<unknown, unknown>
>()
expectTypeOf(queriesData2).toEqualTypeOf<Array<[QueryKey, unknown]>>()

const queryState = queryClient.getQueryState(queryKey)
expectTypeOf(queryState).toEqualTypeOf<
QueryState<unknown, DefaultError> | undefined
>()

const fetchedQuery = await queryClient.fetchQuery(queryOptions)
expectTypeOf(fetchedQuery).toEqualTypeOf<unknown>()

queryClient.prefetchQuery(queryOptions)

const infiniteQuery = await queryClient.fetchInfiniteQuery(
fetchInfiniteQueryOptions,
)
expectTypeOf(infiniteQuery).toEqualTypeOf<InfiniteData<unknown, unknown>>()

const infiniteQueryData = await queryClient.ensureInfiniteQueryData(
fetchInfiniteQueryOptions,
)
expectTypeOf(infiniteQueryData).toEqualTypeOf<
InfiniteData<unknown, unknown>
>()

const defaultQueryOptions = queryClient.defaultQueryOptions(queryOptions)
expectTypeOf(defaultQueryOptions).toEqualTypeOf<
DefaultedQueryObserverOptions<
unknown,
DefaultError,
unknown,
unknown,
QueryKey
>
>()

const mutationOptions2 = queryClient.defaultMutationOptions(mutationOptions)
expectTypeOf(mutationOptions2).toEqualTypeOf<
MutationOptions<unknown, DefaultError, void, unknown>
>()

queryClient.setMutationDefaults(mutationKey, {
onSettled(data, error, variables, context) {
expectTypeOf(data).toEqualTypeOf<unknown>()
expectTypeOf(error).toEqualTypeOf<DefaultError | null>()
expectTypeOf(variables).toEqualTypeOf<void>()
expectTypeOf(context).toEqualTypeOf<unknown>()
},
})

const queryDefaults = queryClient.getQueryDefaults(queryKey)
expectTypeOf(queryDefaults).toEqualTypeOf<
OmitKeyof<QueryObserverOptions<any, any, any, any, any>, 'queryKey'>
>()

// Voids and Untyped returns
queryClient.invalidateQueries(queryFilters)
queryClient.isFetching(queryFilters)
queryClient.isMutating(mutationFilters)
queryClient.removeQueries(queryFilters)
queryClient.resetQueries(queryFilters)
queryClient.cancelQueries(queryFilters)
queryClient.invalidateQueries(queryFilters)
queryClient.refetchQueries(queryFilters)
queryClient.prefetchInfiniteQuery(fetchInfiniteQueryOptions)
queryClient.setQueryDefaults(queryKey, {} as any)
queryClient.getMutationDefaults(mutationKey)
})
})
24 changes: 17 additions & 7 deletions packages/query-core/src/__tests__/utils.test-d.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { describe, expectTypeOf, it } from 'vitest'
import { QueryClient } from '../queryClient'
import type { QueryFilters } from '../utils'
import type { DataTag } from '../types'
import type { DataTag, QueryKey } from '../types'

describe('QueryFilters', () => {
it('should be typed if generics are passed', () => {
type TData = { a: number; b: string }

const a: QueryFilters<TData> = {
const filters: QueryFilters<
TData,
Error,
TData,
QueryKey & DataTag<unknown, TData>
> = {
predicate(query) {
expectTypeOf(query.setData({ a: 1, b: '1' })).toEqualTypeOf<TData>()
return true
Expand All @@ -17,18 +22,23 @@ describe('QueryFilters', () => {

const queryClient = new QueryClient()

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

const error = queryClient.getQueryState(a.queryKey!)?.error
const error = queryClient.getQueryState(filters.queryKey!)?.error
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> = {
const filters: QueryFilters<
TData,
TError,
TData,
QueryKey & DataTag<unknown, TData, TError>
> = {
predicate(query) {
expectTypeOf(query.setData({ a: 1, b: '1' })).toEqualTypeOf<TData>()
return true
Expand All @@ -38,10 +48,10 @@ describe('QueryFilters', () => {

const queryClient = new QueryClient()

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

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

Expand Down
Loading

0 comments on commit 175757a

Please sign in to comment.