diff --git a/src/core/queryClient.ts b/src/core/queryClient.ts index 2a730473d9..2e64194f28 100644 --- a/src/core/queryClient.ts +++ b/src/core/queryClient.ts @@ -536,7 +536,7 @@ export class QueryClient { setQueryDefaults( queryKey: QueryKey, - options: QueryObserverOptions + options: QueryObserverOptions ): void { const result = this.queryDefaults.find( x => hashQueryKey(queryKey) === hashQueryKey(x.queryKey) diff --git a/src/core/tests/hydration.test.tsx b/src/core/tests/hydration.test.tsx index b2730d9740..9182bed093 100644 --- a/src/core/tests/hydration.test.tsx +++ b/src/core/tests/hydration.test.tsx @@ -44,7 +44,7 @@ describe('dehydration and rehydration', () => { key: [{ nestedKey: 1 }], }) - const fetchDataAfterHydration = jest.fn() + const fetchDataAfterHydration = jest.fn() await hydrationClient.prefetchQuery(['string'], fetchDataAfterHydration, { staleTime: 1000, }) @@ -143,7 +143,7 @@ describe('dehydration and rehydration', () => { hydrationCache.find(['string', { key: ['string'], key2: 0 }])?.state.data ).toBe('string') - const fetchDataAfterHydration = jest.fn() + const fetchDataAfterHydration = jest.fn() await hydrationClient.prefetchQuery( ['string', { key: ['string'], key2: 0 }], fetchDataAfterHydration, diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx index 22d6e166a6..75f9d96ace 100644 --- a/src/core/tests/query.test.tsx +++ b/src/core/tests/query.test.tsx @@ -153,7 +153,7 @@ describe('query', () => { const promise = queryClient.fetchQuery( key, - async () => { + async (): Promise => { count++ throw new Error(`error${count}`) }, @@ -282,7 +282,7 @@ describe('query', () => { const key = queryKey() const queryFn = jest.fn< - Promise, + Promise, [QueryFunctionContext>] >() const onAbort = jest.fn() @@ -339,7 +339,7 @@ describe('query', () => { test('should not continue if explicitly cancelled', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(async () => { await sleep(10) @@ -369,7 +369,7 @@ describe('query', () => { test('should not error if reset while loading', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(async () => { await sleep(10) @@ -399,7 +399,7 @@ describe('query', () => { test('should be able to refetch a cancelled query', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(async () => { await sleep(50) @@ -432,9 +432,12 @@ describe('query', () => { test('cancelling a rejected query should not have any effect', async () => { const key = queryKey() - await queryClient.prefetchQuery(key, async () => { - throw new Error('error') - }) + await queryClient.prefetchQuery( + key, + async (): Promise => { + throw new Error('error') + } + ) const query = queryCache.find(key)! query.cancel() await sleep(10) @@ -450,16 +453,20 @@ describe('query', () => { const query = queryCache.find(key)! expect(query.state.status).toBe('success') - await queryClient.prefetchQuery(key, () => Promise.reject('reject'), { - retry: false, - }) + await queryClient.prefetchQuery( + key, + () => Promise.reject('reject'), + { + retry: false, + } + ) expect(query.state.status).toBe('error') queryClient.prefetchQuery( key, async () => { await sleep(10) - return Promise.reject('reject') + return Promise.reject('reject') }, { retry: false } ) diff --git a/src/core/tests/queryCache.test.tsx b/src/core/tests/queryCache.test.tsx index 1097164d2e..e00fbd2827 100644 --- a/src/core/tests/queryCache.test.tsx +++ b/src/core/tests/queryCache.test.tsx @@ -175,7 +175,9 @@ describe('queryCache', () => { const onError = jest.fn() const testCache = new QueryCache({ onError }) const testClient = createQueryClient({ queryCache: testCache }) - await testClient.prefetchQuery(key, () => Promise.reject('error')) + await testClient.prefetchQuery(key, () => + Promise.reject('error') + ) const query = testCache.find(key) expect(onError).toHaveBeenCalledWith('error', query) }) diff --git a/src/core/tests/queryClient.test.tsx b/src/core/tests/queryClient.test.tsx index df24be8319..c6b661ce3c 100644 --- a/src/core/tests/queryClient.test.tsx +++ b/src/core/tests/queryClient.test.tsx @@ -530,9 +530,12 @@ describe('queryClient', () => { const key = queryKey() await expect( - queryClient.fetchQuery(key, async () => { - throw new Error('error') - }) + queryClient.fetchQuery( + key, + async (): Promise => { + throw new Error('error') + } + ) ).rejects.toEqual(new Error('error')) }) @@ -725,7 +728,7 @@ describe('queryClient', () => { const result = await queryClient.prefetchQuery( key, - async () => { + async (): Promise => { throw new Error('error') }, { @@ -783,7 +786,7 @@ describe('queryClient', () => { }) try { await queryClient.fetchQuery(key2, async () => { - return Promise.reject('err') + return Promise.reject('err') }) } catch {} queryClient.fetchQuery(key1, async () => { @@ -793,7 +796,7 @@ describe('queryClient', () => { try { queryClient.fetchQuery(key2, async () => { await sleep(1000) - return Promise.reject('err2') + return Promise.reject('err2') }) } catch {} queryClient.fetchQuery(key3, async () => { @@ -842,7 +845,7 @@ describe('queryClient', () => { describe('refetchQueries', () => { test('should not refetch if all observers are disabled', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') await queryClient.fetchQuery(key, queryFn) const observer1 = new QueryObserver(queryClient, { queryKey: key, @@ -856,7 +859,7 @@ describe('queryClient', () => { }) test('should refetch if at least one observer is enabled', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') await queryClient.fetchQuery(key, queryFn) const observer1 = new QueryObserver(queryClient, { queryKey: key, @@ -878,8 +881,8 @@ describe('queryClient', () => { test('should refetch all queries when no arguments are given', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer1 = new QueryObserver(queryClient, { @@ -904,8 +907,8 @@ describe('queryClient', () => { test('should be able to refetch all fresh queries', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -923,8 +926,8 @@ describe('queryClient', () => { test('should be able to refetch all stale queries', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -943,8 +946,8 @@ describe('queryClient', () => { test('should be able to refetch all stale and active queries', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) queryClient.invalidateQueries(key1) @@ -965,8 +968,8 @@ describe('queryClient', () => { test('should be able to refetch all active and inactive queries', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -984,8 +987,8 @@ describe('queryClient', () => { test('should be able to refetch all active and inactive queries', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -1003,8 +1006,8 @@ describe('queryClient', () => { test('should be able to refetch only active queries', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -1022,8 +1025,8 @@ describe('queryClient', () => { test('should be able to refetch only inactive queries', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -1040,7 +1043,7 @@ describe('queryClient', () => { test('should throw an error if throwOnError option is set to true', async () => { const key1 = queryKey() - const queryFnError = () => Promise.reject('error') + const queryFnError = () => Promise.reject('error') try { await queryClient.fetchQuery({ queryKey: key1, @@ -1065,8 +1068,8 @@ describe('queryClient', () => { test('should refetch active queries by default', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -1084,8 +1087,8 @@ describe('queryClient', () => { test('should not refetch inactive queries by default', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -1103,8 +1106,8 @@ describe('queryClient', () => { test('should not refetch active queries when "refetch" is "none"', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -1124,8 +1127,8 @@ describe('queryClient', () => { test('should refetch inactive queries when "refetch" is "inactive"', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -1147,8 +1150,8 @@ describe('queryClient', () => { test('should refetch active and inactive queries when "refetch" is "all"', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') await queryClient.fetchQuery(key1, queryFn1) await queryClient.fetchQuery(key2, queryFn2) const observer = new QueryObserver(queryClient, { @@ -1275,8 +1278,8 @@ describe('queryClient', () => { test('should refetch all active queries', async () => { const key1 = queryKey() const key2 = queryKey() - const queryFn1 = jest.fn().mockReturnValue('data1') - const queryFn2 = jest.fn().mockReturnValue('data2') + const queryFn1 = jest.fn().mockReturnValue('data1') + const queryFn2 = jest.fn().mockReturnValue('data2') const observer1 = new QueryObserver(queryClient, { queryKey: key1, queryFn: queryFn1, diff --git a/src/core/tests/queryObserver.test.tsx b/src/core/tests/queryObserver.test.tsx index 82af00c784..8c5b4e4092 100644 --- a/src/core/tests/queryObserver.test.tsx +++ b/src/core/tests/queryObserver.test.tsx @@ -26,7 +26,7 @@ describe('queryObserver', () => { test('should trigger a fetch when subscribed', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, queryFn }) const unsubscribe = observer.subscribe(() => undefined) await sleep(1) @@ -308,7 +308,7 @@ describe('queryObserver', () => { test('should not trigger a fetch when subscribed and disabled', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, queryFn, @@ -322,7 +322,7 @@ describe('queryObserver', () => { test('should not trigger a fetch when not subscribed', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') new QueryObserver(queryClient, { queryKey: key, queryFn }) await sleep(1) expect(queryFn).toHaveBeenCalledTimes(0) @@ -330,7 +330,7 @@ describe('queryObserver', () => { test('should be able to watch a query without defining a query function', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') const callback = jest.fn() const observer = new QueryObserver(queryClient, { queryKey: key, @@ -345,7 +345,7 @@ describe('queryObserver', () => { test('should accept unresolved query config in update function', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, @@ -367,7 +367,7 @@ describe('queryObserver', () => { test('should be able to handle multiple subscribers', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') const observer = new QueryObserver(queryClient, { queryKey: key, enabled: false, @@ -400,7 +400,7 @@ describe('queryObserver', () => { queryKey: key, queryFn: () => { count++ - return Promise.reject('reject') + return Promise.reject('reject') }, retry: 10, retryDelay: 50, @@ -467,7 +467,7 @@ describe('queryObserver', () => { queryKey: key, queryFn: () => { count++ - return Promise.reject(`reject ${count}`) + return Promise.reject(`reject ${count}`) }, retry: 1, retryDelay: 20, @@ -519,7 +519,7 @@ describe('queryObserver', () => { const observer = new QueryObserver(queryClient, { queryKey: key, - queryFn: () => Promise.reject('error'), + queryFn: () => Promise.reject('error'), retry: false, }) @@ -535,7 +535,7 @@ describe('queryObserver', () => { test('should not refetch in background if refetchIntervalInBackground is false', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') focusManager.setFocused(false) const observer = new QueryObserver(queryClient, { diff --git a/src/core/types.ts b/src/core/types.ts index 0978cbf36e..47d916d8fc 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -7,14 +7,17 @@ import type { MutationCache } from './mutationCache' import { Logger } from './logger' export type QueryKey = readonly unknown[] -export type QueryFunctionData = T extends undefined ? never : T export type QueryFunction< T = unknown, TQueryKey extends QueryKey = QueryKey > = ( context: QueryFunctionContext -) => QueryFunctionData> +) => [T] extends [undefined] + ? never | 'queryFn must not return undefined or void' + : [T] extends [void] + ? never | 'queryFn must not return undefined or void' + : T | Promise export interface QueryFunctionContext< TQueryKey extends QueryKey = QueryKey, diff --git a/src/reactjs/tests/ssr-hydration.test.tsx b/src/reactjs/tests/ssr-hydration.test.tsx index 531d493438..d6042088b4 100644 --- a/src/reactjs/tests/ssr-hydration.test.tsx +++ b/src/reactjs/tests/ssr-hydration.test.tsx @@ -58,7 +58,10 @@ describe('Server side rendering with de/rehydration', () => { if (!isReact18()) { return } - const fetchDataSuccess = jest.fn(fetchData) + const fetchDataSuccess = jest.fn< + ReturnType, + Parameters + >(fetchData) // -- Shared part -- function SuccessComponent() { @@ -201,7 +204,10 @@ describe('Server side rendering with de/rehydration', () => { if (!isReact18()) { return } - const fetchDataSuccess = jest.fn(fetchData) + const fetchDataSuccess = jest.fn< + ReturnType, + Parameters + >(fetchData) // -- Shared part -- function SuccessComponent() { diff --git a/src/reactjs/tests/ssr.test.tsx b/src/reactjs/tests/ssr.test.tsx index 009c361f06..356e05776b 100644 --- a/src/reactjs/tests/ssr.test.tsx +++ b/src/reactjs/tests/ssr.test.tsx @@ -19,7 +19,7 @@ describe('Server Side Rendering', () => { const queryCache = new QueryCache() const queryClient = createQueryClient({ queryCache }) const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') function Page() { const query = useQuery(key, queryFn) diff --git a/src/reactjs/tests/suspense.test.tsx b/src/reactjs/tests/suspense.test.tsx index 5145ad1b84..97730eefd1 100644 --- a/src/reactjs/tests/suspense.test.tsx +++ b/src/reactjs/tests/suspense.test.tsx @@ -121,7 +121,7 @@ describe("useQuery's in Suspense mode", () => { it('should not call the queryFn twice when used in Suspense mode', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(() => { sleep(10) return 'data' @@ -246,10 +246,17 @@ describe("useQuery's in Suspense mode", () => { } function SecondComponent() { - useQuery(key, () => sleep(20), { - suspense: true, - onSuccess: successFn2, - }) + useQuery( + key, + () => { + sleep(10) + return 'data' + }, + { + suspense: true, + onSuccess: successFn2, + } + ) return second } @@ -572,7 +579,7 @@ describe("useQuery's in Suspense mode", () => { function Page() { useQuery( key, - async () => { + async (): Promise => { await sleep(10) throw new Error('Suspense Error a1x') }, @@ -612,7 +619,7 @@ describe("useQuery's in Suspense mode", () => { function Page() { useQuery( key, - async () => { + async (): Promise => { await sleep(10) throw new Error('Suspense Error a2x') }, @@ -653,7 +660,7 @@ describe("useQuery's in Suspense mode", () => { function Page() { useQuery( key, - async () => { + async (): Promise => { await sleep(10) return Promise.reject('Remote Error') }, @@ -694,7 +701,7 @@ describe("useQuery's in Suspense mode", () => { function Page() { useQuery( key, - async () => { + async (): Promise => { await sleep(10) return Promise.reject('Local Error') }, @@ -732,7 +739,7 @@ describe("useQuery's in Suspense mode", () => { it('should not call the queryFn when not enabled', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn, unknown[]>() queryFn.mockImplementation(async () => { await sleep(10) return '23' diff --git a/src/reactjs/tests/useInfiniteQuery.test.tsx b/src/reactjs/tests/useInfiniteQuery.test.tsx index 25c70d02df..6b1e995a8f 100644 --- a/src/reactjs/tests/useInfiniteQuery.test.tsx +++ b/src/reactjs/tests/useInfiniteQuery.test.tsx @@ -1237,7 +1237,7 @@ describe('useInfiniteQuery', () => { function Page() { const state = useInfiniteQuery( key, - async ({ pageParam }) => { + async ({ pageParam }): Promise => { await sleep(10) return pageParam }, @@ -1340,10 +1340,14 @@ describe('useInfiniteQuery', () => { const states: UseInfiniteQueryResult[] = [] function Page() { - const state = useInfiniteQuery(key, ({ pageParam = 10 }) => pageParam, { - initialData: { pages: [10], pageParams: [undefined] }, - getNextPageParam: lastPage => (lastPage === 10 ? 11 : undefined), - }) + const state = useInfiniteQuery( + key, + ({ pageParam = 10 }): number => pageParam, + { + initialData: { pages: [10], pageParams: [undefined] }, + getNextPageParam: lastPage => (lastPage === 10 ? 11 : undefined), + } + ) states.push(state) @@ -1376,10 +1380,14 @@ describe('useInfiniteQuery', () => { const states: UseInfiniteQueryResult[] = [] function Page() { - const state = useInfiniteQuery(key, ({ pageParam = 10 }) => pageParam, { - initialData: { pages: [10], pageParams: [undefined] }, - getNextPageParam: () => undefined, - }) + const state = useInfiniteQuery( + key, + ({ pageParam = 10 }): number => pageParam, + { + initialData: { pages: [10], pageParams: [undefined] }, + getNextPageParam: () => undefined, + } + ) states.push(state) diff --git a/src/reactjs/tests/useQueries.test.tsx b/src/reactjs/tests/useQueries.test.tsx index 67fc39bf07..7e33c06e30 100644 --- a/src/reactjs/tests/useQueries.test.tsx +++ b/src/reactjs/tests/useQueries.test.tsx @@ -607,6 +607,76 @@ describe('useQueries', () => { // @ts-expect-error (Page component is not rendered) // eslint-disable-next-line function Page() { + // Rejects queryFn that returns/resolved to undefined or void + // @ts-expect-error (queryFn must not return undefined) + useQueries({ queries: [{ queryKey: key1, queryFn: () => undefined }] }) + // @ts-expect-error (queryFn must not return void) + // eslint-disable-next-line @typescript-eslint/no-empty-function + useQueries({ queries: [{ queryKey: key1, queryFn: () => {} }] }) + + useQueries({ + // @ts-expect-error (queryFn must not return explicitly undefined) + queries: [{ queryKey: key1, queryFn: (): undefined => undefined }], + }) + + useQueries({ + // @ts-expect-error (queryFn must not return explicitly void) + queries: [{ queryKey: key1, queryFn: (): void => undefined }], + }) + + useQueries({ + // @ts-expect-error (queryFn must not return explicitly Promise) + queries: [{ queryKey: key1, queryFn: (): Promise => undefined }], + }) + + useQueries({ + queries: [ + // @ts-expect-error (queryFn must not return explicitly Promise) + { queryKey: key1, queryFn: (): Promise => undefined }, + ], + }) + useQueries({ + queries: [ + // @ts-expect-error (queryFn must not return Promise) + { queryKey: key2, queryFn: () => Promise.resolve(undefined) }, + ], + }) + useQueries({ + // @ts-expect-error (queryFn must not return Promise) + queries: Array(50).map((_, i) => ({ + queryKey: ['key', i] as const, + queryFn: () => Promise.resolve(undefined), + })), + }) + + // Rejects queryFn that always throws + useQueries({ + queries: [ + // @ts-expect-error (queryFn must not return undefined) + { + queryKey: key3, + queryFn: async () => { + throw new Error('') + }, + }, + ], + }) + + // Accepts queryFn that *sometimes* throws + useQueries({ + queries: [ + { + queryKey: key3, + queryFn: async () => { + if (Math.random() > 0.1) { + throw new Error('') + } + return 'result' + }, + }, + ], + }) + // Array.map preserves TQueryFnData const result1 = useQueries({ queries: Array(50).map((_, i) => ({ diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index a22f1601d3..bd234834f8 100644 --- a/src/reactjs/tests/useQuery.test.tsx +++ b/src/reactjs/tests/useQuery.test.tsx @@ -42,6 +42,18 @@ describe('useQuery', () => { // @ts-expect-error (queryFn returns undefined) useQuery(key, () => undefined) + // it should not be possible for queryFn to have explicit void return type + // @ts-expect-error (queryFn explicit return type is void) + useQuery(key, (): void => undefined) + + // it should not be possible for queryFn to have explicit Promise return type + // @ts-expect-error (queryFn explicit return type is Promise) + useQuery(key, (): Promise => Promise.resolve()) + + // it should not be possible for queryFn to have explicit Promise return type + // @ts-expect-error (queryFn explicit return type is Promise) + useQuery(key, (): Promise => Promise.resolve(undefined)) + // it should infer the result type from the query function const fromQueryFn = useQuery(key, () => 'test') expectType(fromQueryFn.data) @@ -63,6 +75,24 @@ describe('useQuery', () => { onSettled: data => expectType(data), }) + // it should be possible to specify a union type as result type + const unionTypeSync = useQuery( + key, + () => (Math.random() > 0.5 ? 'a' : 'b'), + { + onSuccess: data => expectType<'a' | 'b'>(data), + } + ) + expectType<'a' | 'b' | undefined>(unionTypeSync.data) + const unionTypeAsync = useQuery<'a' | 'b'>( + key, + () => Promise.resolve(Math.random() > 0.5 ? 'a' : 'b'), + { + onSuccess: data => expectType<'a' | 'b'>(data), + } + ) + expectType<'a' | 'b' | undefined>(unionTypeAsync.data) + // should error when the query function result does not match with the specified type // @ts-expect-error useQuery(key, () => 'test') @@ -563,7 +593,7 @@ describe('useQuery', () => { const onSettled = jest.fn() function Page() { - const state = useQuery(key, () => Promise.reject('error'), { + const state = useQuery(key, () => Promise.reject('error'), { retry: false, onSettled, }) @@ -2485,7 +2515,7 @@ describe('useQuery', () => { it('should not refetch query on focus when `enabled` is set to `false`', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') function Page() { const { data = 'default' } = useQuery(key, queryFn, { @@ -2757,7 +2787,7 @@ describe('useQuery', () => { const key = queryKey() function Page() { - const { status, error } = useQuery( + const { status, error } = useQuery( key, () => { return Promise.reject('Error test jaylen') @@ -2783,7 +2813,7 @@ describe('useQuery', () => { const key = queryKey() function Page() { - const { status, error } = useQuery( + const { status, error } = useQuery( key, () => Promise.reject('Error test jaylen'), { retry: false, useErrorBoundary: true } @@ -2835,7 +2865,7 @@ describe('useQuery', () => { const key = queryKey() function Page() { - const { status, error } = useQuery( + const { status, error } = useQuery( key, () => Promise.reject('Local Error'), { @@ -2867,7 +2897,7 @@ describe('useQuery', () => { const key = queryKey() function Page() { - const { status, error } = useQuery( + const { status, error } = useQuery( key, () => Promise.reject(new Error('Remote Error')), { @@ -3210,7 +3240,7 @@ describe('useQuery', () => { it('should retry specified number of times', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(() => { return Promise.reject('Error test Barrett') }) @@ -3243,7 +3273,7 @@ describe('useQuery', () => { it('should not retry if retry function `false`', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementationOnce(() => { return Promise.reject('Error test Tanner') @@ -3255,7 +3285,7 @@ describe('useQuery', () => { function Page() { const { status, failureCount, error } = useQuery< - undefined, + unknown, string, [string] >(key, queryFn, { @@ -3288,7 +3318,7 @@ describe('useQuery', () => { type DelayError = { delay: number } - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(() => { return Promise.reject({ delay: 50 }) }) @@ -3332,7 +3362,7 @@ describe('useQuery', () => { key, () => { count++ - return Promise.reject(`fetching error ${count}`) + return Promise.reject(`fetching error ${count}`) }, { retry: 3, @@ -3474,10 +3504,10 @@ describe('useQuery', () => { const key = queryKey() const states: UseQueryResult[] = [] - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(() => 'data') - const prefetchQueryFn = jest.fn() + const prefetchQueryFn = jest.fn() prefetchQueryFn.mockImplementation(() => 'not yet...') await queryClient.prefetchQuery(key, prefetchQueryFn, { @@ -3503,10 +3533,10 @@ describe('useQuery', () => { it('should not refetch if not stale after a prefetch', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(() => 'data') - const prefetchQueryFn = jest.fn() + const prefetchQueryFn = jest.fn, unknown[]>() prefetchQueryFn.mockImplementation(async () => { await sleep(10) return 'not yet...' @@ -3723,7 +3753,7 @@ describe('useQuery', () => { it('it should support enabled:false in query object syntax', async () => { const key = queryKey() - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(() => 'data') function Page() { @@ -3788,7 +3818,7 @@ describe('useQuery', () => { it('should not cause memo churn when data does not change', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') const memoFn = jest.fn() function Page() { @@ -3985,7 +4015,7 @@ describe('useQuery', () => { it('should refetch if any query instance becomes enabled', async () => { const key = queryKey() - const queryFn = jest.fn().mockReturnValue('data') + const queryFn = jest.fn().mockReturnValue('data') function Disabled() { useQuery(key, queryFn, { enabled: false }) @@ -4673,7 +4703,7 @@ describe('useQuery', () => { }) it('should refetch when changed enabled to true in error state', async () => { - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(async () => { await sleep(10) return Promise.reject(new Error('Suspense Error Bingo')) @@ -4794,7 +4824,7 @@ describe('useQuery', () => { [id], async () => { await sleep(10) - return Promise.reject(new Error('Error')) + return Promise.reject(new Error('Error')) }, { retry: false, @@ -5263,7 +5293,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { + queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) @@ -5558,7 +5588,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { + queryFn: async (): Promise => { count++ await sleep(10) throw new Error('error ' + count) @@ -5604,7 +5634,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { + queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) @@ -5649,10 +5679,10 @@ describe('useQuery', () => { it('it should have status=error on mount when a query has failed', async () => { const key = queryKey() - const states: UseQueryResult[] = [] + const states: UseQueryResult[] = [] const error = new Error('oops') - const queryFn = async () => { + const queryFn = async (): Promise => { throw error } diff --git a/src/reactjs/useQueries.ts b/src/reactjs/useQueries.ts index 97f4cd6c5d..10ed424c71 100644 --- a/src/reactjs/useQueries.ts +++ b/src/reactjs/useQueries.ts @@ -17,6 +17,10 @@ type UseQueryOptionsForUseQueries< TQueryKey extends QueryKey = QueryKey > = Omit, 'context'> +type InvalidQueryFn = QueryFunction< + undefined | Promise | void | Promise +> + // Avoid TS depth-limit error in case of large array literal type MAXIMUM_DEPTH = 20 @@ -40,7 +44,9 @@ type GetOptions = : T extends [infer TQueryFnData] ? UseQueryOptionsForUseQueries : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided - T extends { + T extends { queryFn?: InvalidQueryFn } + ? never | 'queryFn must not return undefined or void' + : T extends { queryFn?: QueryFunction select: (data: any) => infer TData } @@ -72,7 +78,7 @@ type GetResults = ? UseQueryResult : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided T extends { - queryFn?: QueryFunction + queryFn?: QueryFunction select: (data: any) => infer TData } ? UseQueryResult @@ -100,7 +106,9 @@ export type QueriesOptions< ? T : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! // use this to infer the param types in the case of Array.map() argument - T extends UseQueryOptionsForUseQueries< + T extends { queryFn: InvalidQueryFn }[] + ? (never | 'queryFn must not return undefined or void')[] + : T extends UseQueryOptionsForUseQueries< infer TQueryFnData, infer TError, infer TData,