From 32f11adc820d03482ffcfd20ddd51f2de0760f2c Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Thu, 21 Apr 2022 08:31:21 +0200 Subject: [PATCH 01/10] fix(types): fix handling of promise return type in QueryFunctionData --- src/core/types.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/types.ts b/src/core/types.ts index d096a79412..88a20b6cde 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -7,7 +7,13 @@ import type { MutationCache } from './mutationCache' import { Logger } from './logger' export type QueryKey = readonly unknown[] -export type QueryFunctionData = T extends undefined ? never : T +export type QueryFunctionData = TReturn extends Promise< + infer TPromiseReturn +> + ? Promise> + : TReturn extends undefined + ? never + : TReturn export type QueryFunction< T = unknown, From f421cc93f463d4bb1ae282a56b13e52f14144f22 Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Thu, 21 Apr 2022 22:27:39 +0200 Subject: [PATCH 02/10] fix(QueryFunction): return type restrict void and avoid union type distribution --- src/core/types.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/core/types.ts b/src/core/types.ts index 88a20b6cde..bc536dabcc 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -7,20 +7,17 @@ import type { MutationCache } from './mutationCache' import { Logger } from './logger' export type QueryKey = readonly unknown[] -export type QueryFunctionData = TReturn extends Promise< - infer TPromiseReturn -> - ? Promise> - : TReturn extends undefined - ? never - : TReturn export type QueryFunction< T = unknown, TQueryKey extends QueryKey = QueryKey > = ( context: QueryFunctionContext -) => QueryFunctionData> +) => [T] extends [undefined] + ? never + : [T] extends [void] + ? never + : T | Promise export interface QueryFunctionContext< TQueryKey extends QueryKey = QueryKey, From 59d6fd599b7b62a012ea676b7b4fd70c697d5920 Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Thu, 21 Apr 2022 22:28:18 +0200 Subject: [PATCH 03/10] fix(QueryClient): make setQueryDefaults compatible with adjusted QueryFunction type --- src/core/queryClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From 523a7c352d6fac4b5d779f1048f4909ff4231b74 Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Thu, 21 Apr 2022 22:28:55 +0200 Subject: [PATCH 04/10] fix(useQueries): correct type inferrence in GetResults with new QueryFunction type --- src/reactjs/useQueries.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reactjs/useQueries.ts b/src/reactjs/useQueries.ts index 95a29d1775..c4856fcbed 100644 --- a/src/reactjs/useQueries.ts +++ b/src/reactjs/useQueries.ts @@ -72,7 +72,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 From 7fbf570053e76c0461a882fe0161a8998be82e1f Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Thu, 21 Apr 2022 22:29:57 +0200 Subject: [PATCH 05/10] fix(tests): adjust all test cases which use any or never as query function return type to comply with new QueryFunction type --- src/core/tests/hydration.test.tsx | 4 +- src/core/tests/query.test.tsx | 31 +++++---- src/core/tests/queryCache.test.tsx | 4 +- src/core/tests/queryClient.test.tsx | 77 +++++++++++---------- src/core/tests/queryObserver.test.tsx | 20 +++--- src/reactjs/tests/ssr-hydration.test.tsx | 10 ++- src/reactjs/tests/ssr.test.tsx | 2 +- src/reactjs/tests/suspense.test.tsx | 27 +++++--- src/reactjs/tests/useInfiniteQuery.test.tsx | 26 ++++--- src/reactjs/tests/useQuery.test.tsx | 46 ++++++------ 10 files changed, 140 insertions(+), 107 deletions(-) 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..b8cb6d1c75 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,14 +453,18 @@ 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 () => { + async (): Promise => { await sleep(10) return Promise.reject('reject') }, 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/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/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index af246055c3..618f101693 100644 --- a/src/reactjs/tests/useQuery.test.tsx +++ b/src/reactjs/tests/useQuery.test.tsx @@ -553,7 +553,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, }) @@ -2475,7 +2475,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, { @@ -2666,7 +2666,7 @@ describe('useQuery', () => { const key = queryKey() function Page() { - const { status, error } = useQuery( + const { status, error } = useQuery( key, () => { return Promise.reject('Error test jaylen') @@ -2692,7 +2692,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 } @@ -2744,7 +2744,7 @@ describe('useQuery', () => { const key = queryKey() function Page() { - const { status, error } = useQuery( + const { status, error } = useQuery( key, () => Promise.reject('Local Error'), { @@ -2776,7 +2776,7 @@ describe('useQuery', () => { const key = queryKey() function Page() { - const { status, error } = useQuery( + const { status, error } = useQuery( key, () => Promise.reject(new Error('Remote Error')), { @@ -3119,7 +3119,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') }) @@ -3152,7 +3152,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') @@ -3164,7 +3164,7 @@ describe('useQuery', () => { function Page() { const { status, failureCount, error } = useQuery< - undefined, + unknown, string, [string] >(key, queryFn, { @@ -3197,7 +3197,7 @@ describe('useQuery', () => { type DelayError = { delay: number } - const queryFn = jest.fn() + const queryFn = jest.fn() queryFn.mockImplementation(() => { return Promise.reject({ delay: 50 }) }) @@ -3241,7 +3241,7 @@ describe('useQuery', () => { key, () => { count++ - return Promise.reject(`fetching error ${count}`) + return Promise.reject(`fetching error ${count}`) }, { retry: 3, @@ -3383,10 +3383,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, { @@ -3412,10 +3412,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...' @@ -3632,7 +3632,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() { @@ -3697,7 +3697,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() { @@ -3894,7 +3894,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 }) @@ -4582,7 +4582,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')) @@ -4703,7 +4703,7 @@ describe('useQuery', () => { [id], async () => { await sleep(10) - return Promise.reject(new Error('Error')) + return Promise.reject(new Error('Error')) }, { retry: false, @@ -5172,7 +5172,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { + queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) @@ -5467,7 +5467,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { + queryFn: async (): Promise => { count++ await sleep(10) throw new Error('error ' + count) @@ -5513,7 +5513,7 @@ describe('useQuery', () => { function Page() { const state = useQuery({ queryKey: key, - queryFn: async () => { + queryFn: async (): Promise => { count++ await sleep(10) throw new Error('failed' + count) From 3481b40ef695b8d06333f39965ff9967ebfa6202 Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Thu, 21 Apr 2022 22:33:43 +0200 Subject: [PATCH 06/10] fix(tests): change incorrect query function return types of query tests from string to unknown --- src/core/tests/query.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/tests/query.test.tsx b/src/core/tests/query.test.tsx index b8cb6d1c75..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 (): Promise => { + 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() @@ -434,7 +434,7 @@ describe('query', () => { await queryClient.prefetchQuery( key, - async (): Promise => { + async (): Promise => { throw new Error('error') } ) @@ -464,9 +464,9 @@ describe('query', () => { queryClient.prefetchQuery( key, - async (): Promise => { + async () => { await sleep(10) - return Promise.reject('reject') + return Promise.reject('reject') }, { retry: false } ) From ca70f08f3ec8a6d4d9ae06e3b41dcdb9086238d6 Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Thu, 21 Apr 2022 22:55:21 +0200 Subject: [PATCH 07/10] feature(useQuery): add test cases covering void and Promise return types --- src/reactjs/tests/useQuery.test.tsx | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index 618f101693..fb21f2bd1e 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') From 201cae11a447b34be37d504e51ebf7e7ac69a0df Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Fri, 22 Apr 2022 16:53:38 +0200 Subject: [PATCH 08/10] feature(useQueries): reject void or undefined as query function return type --- src/core/types.ts | 4 ++-- src/reactjs/useQueries.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/core/types.ts b/src/core/types.ts index bc536dabcc..f8ee71537a 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -14,9 +14,9 @@ export type QueryFunction< > = ( context: QueryFunctionContext ) => [T] extends [undefined] - ? never + ? never | 'queryFn must not return undefined or void' : [T] extends [void] - ? never + ? never | 'queryFn must not return undefined or void' : T | Promise export interface QueryFunctionContext< diff --git a/src/reactjs/useQueries.ts b/src/reactjs/useQueries.ts index c4856fcbed..e4f7e6b182 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 } @@ -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, From b9168c2eded52b9e53f88d6cd4462463e2a315a9 Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Fri, 22 Apr 2022 16:54:53 +0200 Subject: [PATCH 09/10] feature(useQueries): add test cases for invalid query function return types --- src/reactjs/tests/useQueries.test.tsx | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) 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) => ({ From 6984adc3b6d4bfa7e35a679675d5c88cf1021c0e Mon Sep 17 00:00:00 2001 From: Yannick Stachelscheid Date: Wed, 4 May 2022 19:33:28 +0200 Subject: [PATCH 10/10] fix tsc error after rebasing lastest beta branch --- src/reactjs/tests/useQuery.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reactjs/tests/useQuery.test.tsx b/src/reactjs/tests/useQuery.test.tsx index c113e3300d..bd234834f8 100644 --- a/src/reactjs/tests/useQuery.test.tsx +++ b/src/reactjs/tests/useQuery.test.tsx @@ -5679,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 }