diff --git a/packages/query-core/src/__tests__/queryClient.test-d.tsx b/packages/query-core/src/__tests__/queryClient.test-d.tsx index 8ff728c3db..4f0d42fc84 100644 --- a/packages/query-core/src/__tests__/queryClient.test-d.tsx +++ b/packages/query-core/src/__tests__/queryClient.test-d.tsx @@ -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', () => { @@ -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 = { + queryKey: ['key'] as any, + } + const fetchInfiniteQueryOptions: FetchInfiniteQueryOptions = + { + queryKey: ['key'] as any, + pages: 5, + getNextPageParam: (lastPage) => { + expectTypeOf(lastPage).toEqualTypeOf() + return 0 + }, + initialPageParam: 0, + } + const mutationOptions: MutationOptions = {} + + const queryFilters: QueryFilters< + TData, + TError, + TData, + QueryKey & DataTag + > = { + predicate(query) { + expectTypeOf(query).toEqualTypeOf< + Query< + TData, + TError, + TData, + QueryKey & DataTag + > + >() + expectTypeOf(query.state.data).toEqualTypeOf() + expectTypeOf(query.state.error).toEqualTypeOf() + return false + }, + } + const queryKey = queryFilters.queryKey! + + const mutationFilters: MutationFilters = { + predicate(mutation) { + expectTypeOf(mutation).toEqualTypeOf>() + expectTypeOf(mutation.state.data).toEqualTypeOf() + expectTypeOf(mutation.state.error).toEqualTypeOf() + return false + }, + } + const mutationKey = mutationOptions.mutationKey! + + // + // Method type tests + // + + const state = queryClient.getQueryState(queryKey) + expectTypeOf(state).toEqualTypeOf | undefined>() + + const queryData1 = queryClient.getQueryData(queryKey) + expectTypeOf(queryData1).toEqualTypeOf() + + const queryData2 = await queryClient.ensureQueryData(queryOptions) + expectTypeOf(queryData2).toEqualTypeOf() + + const queriesData = queryClient.getQueriesData(queryFilters) + expectTypeOf(queriesData).toEqualTypeOf< + Array<[QueryKey, TData | undefined]> + >() + + const queryData3 = queryClient.setQueryData(queryKey, { foo: '' }) + type SetQueryDataUpdaterArg = Parameters< + typeof queryClient.setQueryData + >[1] + + expectTypeOf().toEqualTypeOf< + Updater + >() + expectTypeOf(queryData3).toEqualTypeOf() + + const queriesData2 = queryClient.setQueriesData(queryFilters, { foo: '' }) // TODO: types here are wrong and coming up undefined + type SetQueriesDataUpdaterArg = Parameters< + typeof queryClient.setQueriesData + >[1] + + expectTypeOf().toEqualTypeOf< + Updater + >() + expectTypeOf(queriesData2).toEqualTypeOf< + Array<[QueryKey, TData | undefined]> + >() + + const queryState = queryClient.getQueryState(queryKey) + expectTypeOf(queryState).toEqualTypeOf< + QueryState | undefined + >() + + const fetchedQuery = await queryClient.fetchQuery(queryOptions) + expectTypeOf(fetchedQuery).toEqualTypeOf() + + queryClient.prefetchQuery(queryOptions) + + const infiniteQuery = await queryClient.fetchInfiniteQuery( + fetchInfiniteQueryOptions, + ) + expectTypeOf(infiniteQuery).toEqualTypeOf>() + + const infiniteQueryData = await queryClient.ensureInfiniteQueryData( + fetchInfiniteQueryOptions, + ) + expectTypeOf(infiniteQueryData).toEqualTypeOf< + InfiniteData + >() + + const defaultQueryOptions = queryClient.defaultQueryOptions(queryOptions) + expectTypeOf(defaultQueryOptions).toEqualTypeOf< + DefaultedQueryObserverOptions + >() + + const mutationOptions2 = queryClient.defaultMutationOptions(mutationOptions) + expectTypeOf(mutationOptions2).toEqualTypeOf< + MutationOptions + >() + + queryClient.setMutationDefaults(mutationKey, { + onSettled(data, error, variables, context) { + expectTypeOf(data).toEqualTypeOf() + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + }, + }) + + const queryDefaults = queryClient.getQueryDefaults(queryKey) + expectTypeOf(queryDefaults).toEqualTypeOf< + OmitKeyof, '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() + return 0 + }, + initialPageParam: 0, + } + const mutationOptions: MutationOptions = {} + + const queryFilters: QueryFilters = { + predicate(query) { + expectTypeOf(query).toEqualTypeOf>() + expectTypeOf(query.state.data).toEqualTypeOf() + expectTypeOf(query.state.error).toEqualTypeOf() + return false + }, + } + const queryKey = queryFilters.queryKey! + + const mutationFilters: MutationFilters = { + predicate(mutation) { + expectTypeOf(mutation).toEqualTypeOf() + expectTypeOf(mutation.state.data).toEqualTypeOf() + expectTypeOf(mutation.state.error).toEqualTypeOf() + return false + }, + } + const mutationKey = mutationOptions.mutationKey! + + // + // Method type tests + // + + const state = queryClient.getQueryState(queryKey) + expectTypeOf(state).toEqualTypeOf< + QueryState | undefined + >() + + const queryData1 = queryClient.getQueryData(queryKey) + expectTypeOf(queryData1).toEqualTypeOf() + + const queryData2 = await queryClient.ensureQueryData(queryOptions) + expectTypeOf(queryData2).toEqualTypeOf() + + const queriesData = queryClient.getQueriesData(queryFilters) + expectTypeOf(queriesData).toEqualTypeOf>() + + const queryData3 = queryClient.setQueryData(queryKey, { foo: '' }) + type SetQueryDataUpdaterArg = Parameters< + typeof queryClient.setQueryData + >[1] + + expectTypeOf().toEqualTypeOf< + Updater + >() + expectTypeOf(queryData3).toEqualTypeOf() + + const queriesData2 = queryClient.setQueriesData(queryFilters, { foo: '' }) // TODO: types here are wrong and coming up undefined + type SetQueriesDataUpdaterArg = Parameters< + typeof queryClient.setQueriesData + >[1] + + expectTypeOf().toEqualTypeOf< + Updater + >() + expectTypeOf(queriesData2).toEqualTypeOf>() + + const queryState = queryClient.getQueryState(queryKey) + expectTypeOf(queryState).toEqualTypeOf< + QueryState | undefined + >() + + const fetchedQuery = await queryClient.fetchQuery(queryOptions) + expectTypeOf(fetchedQuery).toEqualTypeOf() + + queryClient.prefetchQuery(queryOptions) + + const infiniteQuery = await queryClient.fetchInfiniteQuery( + fetchInfiniteQueryOptions, + ) + expectTypeOf(infiniteQuery).toEqualTypeOf>() + + const infiniteQueryData = await queryClient.ensureInfiniteQueryData( + fetchInfiniteQueryOptions, + ) + expectTypeOf(infiniteQueryData).toEqualTypeOf< + InfiniteData + >() + + const defaultQueryOptions = queryClient.defaultQueryOptions(queryOptions) + expectTypeOf(defaultQueryOptions).toEqualTypeOf< + DefaultedQueryObserverOptions< + unknown, + DefaultError, + unknown, + unknown, + QueryKey + > + >() + + const mutationOptions2 = queryClient.defaultMutationOptions(mutationOptions) + expectTypeOf(mutationOptions2).toEqualTypeOf< + MutationOptions + >() + + queryClient.setMutationDefaults(mutationKey, { + onSettled(data, error, variables, context) { + expectTypeOf(data).toEqualTypeOf() + expectTypeOf(error).toEqualTypeOf() + expectTypeOf(variables).toEqualTypeOf() + expectTypeOf(context).toEqualTypeOf() + }, + }) + + const queryDefaults = queryClient.getQueryDefaults(queryKey) + expectTypeOf(queryDefaults).toEqualTypeOf< + OmitKeyof, '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) + }) +}) diff --git a/packages/query-core/src/__tests__/utils.test-d.tsx b/packages/query-core/src/__tests__/utils.test-d.tsx index 7a694453a7..2c602ddc3b 100644 --- a/packages/query-core/src/__tests__/utils.test-d.tsx +++ b/packages/query-core/src/__tests__/utils.test-d.tsx @@ -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 = { + const filters: QueryFilters< + TData, + Error, + TData, + QueryKey & DataTag + > = { predicate(query) { expectTypeOf(query.setData({ a: 1, b: '1' })).toEqualTypeOf() return true @@ -17,10 +22,10 @@ describe('QueryFilters', () => { const queryClient = new QueryClient() - const data = queryClient.getQueryData(a.queryKey!) + const data = queryClient.getQueryData(filters.queryKey!) expectTypeOf(data).toEqualTypeOf() - const error = queryClient.getQueryState(a.queryKey!)?.error + const error = queryClient.getQueryState(filters.queryKey!)?.error expectTypeOf(error).toEqualTypeOf() }) @@ -28,7 +33,12 @@ describe('QueryFilters', () => { type TData = { a: number; b: string } type TError = Error & { message: string } - const a: QueryFilters = { + const filters: QueryFilters< + TData, + TError, + TData, + QueryKey & DataTag + > = { predicate(query) { expectTypeOf(query.setData({ a: 1, b: '1' })).toEqualTypeOf() return true @@ -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() - const error = queryClient.getQueryState(a.queryKey!)?.error + const error = queryClient.getQueryState(filters.queryKey!)?.error expectTypeOf(error).toEqualTypeOf() }) diff --git a/packages/query-core/src/queryClient.ts b/packages/query-core/src/queryClient.ts index 04bc37ab57..86d551b0f3 100644 --- a/packages/query-core/src/queryClient.ts +++ b/packages/query-core/src/queryClient.ts @@ -106,12 +106,16 @@ export class QueryClient { this.#unsubscribeOnline = undefined } - isFetching(filters?: QueryFilters): number { + isFetching< + TQueryFilters extends QueryFilters = QueryFilters, + >(filters?: TQueryFilters): number { return this.#queryCache.findAll({ ...filters, fetchStatus: 'fetching' }) .length } - isMutating(filters?: MutationFilters): number { + isMutating< + TMutationFilters extends MutationFilters = MutationFilters, + >(filters?: TMutationFilters): number { return this.#mutationCache.findAll({ ...filters, status: 'pending' }).length } @@ -125,10 +129,12 @@ export class QueryClient { > ? TaggedValue : TQueryFnData, - >(queryKey: TTaggedQueryKey): TInferredQueryFnData | undefined - getQueryData(queryKey: QueryKey) { + >(queryKey: TTaggedQueryKey): TInferredQueryFnData | undefined { const options = this.defaultQueryOptions({ queryKey }) - return this.#queryCache.get(options.queryHash)?.state.data + + return this.#queryCache.get(options.queryHash)?.state.data as + | TInferredQueryFnData + | undefined } ensureQueryData< @@ -141,8 +147,9 @@ export class QueryClient { ): Promise { const cachedData = this.getQueryData(options.queryKey) - if (cachedData === undefined) return this.fetchQuery(options) - else { + if (cachedData === undefined) { + return this.fetchQuery(options) + } else { const defaultedOptions = this.defaultQueryOptions(options) const query = this.#queryCache.build(this, defaultedOptions) @@ -157,11 +164,27 @@ export class QueryClient { } } - getQueriesData( - filters: QueryFilters, - ): Array<[QueryKey, TQueryFnData | undefined]> { + getQueriesData< + TQueryFnData = unknown, + TQueryFilters extends QueryFilters< + any, + any, + any, + any + > = QueryFilters, + TInferredQueryFnData = TQueryFilters extends QueryFilters< + infer TData, + any, + any, + any + > + ? TData + : TQueryFnData, + >( + filters: TQueryFilters, + ): Array<[QueryKey, TInferredQueryFnData | undefined]> { return this.#queryCache.findAll(filters).map(({ queryKey, state }) => { - const data = state.data as TQueryFnData | undefined + const data = state.data as TInferredQueryFnData | undefined return [queryKey, data] }) } @@ -207,17 +230,36 @@ export class QueryClient { .setData(data, { ...options, manual: true }) } - setQueriesData( - filters: QueryFilters, - updater: Updater, + setQueriesData< + TQueryFnData, + TQueryFilters extends QueryFilters< + any, + any, + any, + any + > = QueryFilters, + TInferredQueryFnData = TQueryFilters extends QueryFilters< + infer TData, + any, + any, + any + > + ? TData + : TQueryFnData, + >( + filters: TQueryFilters, + updater: Updater< + NoInfer | undefined, + NoInfer | undefined + >, options?: SetDataOptions, - ): Array<[QueryKey, TQueryFnData | undefined]> { + ): Array<[QueryKey, TInferredQueryFnData | undefined]> { return notifyManager.batch(() => this.#queryCache .findAll(filters) .map(({ queryKey }) => [ queryKey, - this.setQueryData(queryKey, updater, options), + this.setQueryData(queryKey, updater, options), ]), ) } @@ -251,7 +293,9 @@ export class QueryClient { )?.state } - removeQueries(filters?: QueryFilters): void { + removeQueries< + TQueryFilters extends QueryFilters = QueryFilters, + >(filters?: TQueryFilters): void { const queryCache = this.#queryCache notifyManager.batch(() => { queryCache.findAll(filters).forEach((query) => { @@ -260,7 +304,9 @@ export class QueryClient { }) } - resetQueries(filters?: QueryFilters, options?: ResetOptions): Promise { + resetQueries< + TQueryFilters extends QueryFilters = QueryFilters, + >(filters?: TQueryFilters, options?: ResetOptions): Promise { const queryCache = this.#queryCache const refetchFilters: RefetchQueryFilters = { @@ -276,10 +322,9 @@ export class QueryClient { }) } - cancelQueries( - filters: QueryFilters = {}, - cancelOptions: CancelOptions = {}, - ): Promise { + cancelQueries< + TQueryFilters extends QueryFilters = QueryFilters, + >(filters?: TQueryFilters, cancelOptions: CancelOptions = {}): Promise { const defaultedCancelOptions = { revert: true, ...cancelOptions } const promises = notifyManager.batch(() => @@ -291,8 +336,15 @@ export class QueryClient { return Promise.all(promises).then(noop).catch(noop) } - invalidateQueries( - filters: InvalidateQueryFilters = {}, + invalidateQueries< + TInvalidateQueryFilters extends InvalidateQueryFilters< + any, + any, + any, + any + > = InvalidateQueryFilters, + >( + filters?: TInvalidateQueryFilters, options: InvalidateOptions = {}, ): Promise { return notifyManager.batch(() => { @@ -300,24 +352,31 @@ export class QueryClient { query.invalidate() }) - if (filters.refetchType === 'none') { + if (filters?.refetchType === 'none') { return Promise.resolve() } const refetchFilters: RefetchQueryFilters = { ...filters, - type: filters.refetchType ?? filters.type ?? 'active', + type: filters?.refetchType ?? filters?.type ?? 'active', } return this.refetchQueries(refetchFilters, options) }) } - refetchQueries( - filters: RefetchQueryFilters = {}, - options?: RefetchOptions, + refetchQueries< + TRefetchQueryFilters extends RefetchQueryFilters< + any, + any, + any, + any + > = RefetchQueryFilters, + >( + filters?: TRefetchQueryFilters, + options: RefetchOptions = {}, ): Promise { const fetchOptions = { ...options, - cancelRefetch: options?.cancelRefetch ?? true, + cancelRefetch: options.cancelRefetch ?? true, } const promises = notifyManager.batch(() => this.#queryCache @@ -494,14 +553,14 @@ export class QueryClient { ): OmitKeyof, 'queryKey'> { const defaults = [...this.#queryDefaults.values()] - let result: OmitKeyof< + const result: OmitKeyof< QueryObserverOptions, 'queryKey' > = {} defaults.forEach((queryDefault) => { if (partialMatchKey(queryKey, queryDefault.queryKey)) { - result = { ...result, ...queryDefault.defaultOptions } + Object.assign(result, queryDefault.defaultOptions) } }) return result diff --git a/packages/query-core/src/utils.ts b/packages/query-core/src/utils.ts index c549a311b7..bcc35239f1 100644 --- a/packages/query-core/src/utils.ts +++ b/packages/query-core/src/utils.ts @@ -1,5 +1,4 @@ import type { - DataTag, DefaultError, Enabled, FetchStatus, @@ -36,9 +35,7 @@ export interface QueryFilters< /** * Include queries matching this query key */ - queryKey?: unknown extends TQueryFnData - ? QueryKey - : QueryKey & DataTag + queryKey?: TQueryKey /** * Include or exclude stale queries */ @@ -49,7 +46,12 @@ export interface QueryFilters< fetchStatus?: FetchStatus } -export interface MutationFilters { +export interface MutationFilters< + TData = unknown, + TError = DefaultError, + TVariables = unknown, + TContext = unknown, +> { /** * Match mutation key exactly */ @@ -57,7 +59,9 @@ export interface MutationFilters { /** * Include mutations matching this predicate function */ - predicate?: (mutation: Mutation) => boolean + predicate?: ( + mutation: Mutation, + ) => boolean /** * Include mutations matching this mutation key */