From 466017e7ef70c86caf5efeac8007193dd4c67c13 Mon Sep 17 00:00:00 2001 From: johnnyreilly Date: Sat, 26 Dec 2020 19:36:39 +0000 Subject: [PATCH 1/7] feat(types): useQueries to flow through types --- src/core/queriesObserver.ts | 42 ++++++++++++++++++++--------- src/react/tests/useQueries.test.tsx | 16 ++++++++++- src/react/useQueries.ts | 15 ++++++++--- 3 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/core/queriesObserver.ts b/src/core/queriesObserver.ts index 39c3a05ee6..ff72219a7c 100644 --- a/src/core/queriesObserver.ts +++ b/src/core/queriesObserver.ts @@ -5,15 +5,26 @@ import type { QueryClient } from './queryClient' import { QueryObserver } from './queryObserver' import { Subscribable } from './subscribable' -type QueriesObserverListener = (result: QueryObserverResult[]) => void - -export class QueriesObserver extends Subscribable { +export class QueriesObserver< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData, + TQueryData = TQueryFnData +> extends Subscribable<(result: QueryObserverResult[]) => void> { private client: QueryClient - private result: QueryObserverResult[] - private queries: QueryObserverOptions[] - private observers: QueryObserver[] - - constructor(client: QueryClient, queries?: QueryObserverOptions[]) { + private result: QueryObserverResult[] + private queries: QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData + >[] + private observers: QueryObserver[] + + constructor( + client: QueryClient, + queries?: QueryObserverOptions[] + ) { super() this.client = client @@ -48,12 +59,14 @@ export class QueriesObserver extends Subscribable { }) } - setQueries(queries: QueryObserverOptions[]): void { + setQueries( + queries: QueryObserverOptions[] + ): void { this.queries = queries this.updateObservers() } - getCurrentResult(): QueryObserverResult[] { + getCurrentResult(): QueryObserverResult[] { return this.result } @@ -62,7 +75,9 @@ export class QueriesObserver extends Subscribable { const prevObservers = this.observers const newObservers = this.queries.map((options, i) => { - let observer: QueryObserver | undefined = prevObservers[i] + let observer: + | QueryObserver + | undefined = prevObservers[i] const defaultedOptions = this.client.defaultQueryObserverOptions(options) const hashFn = getQueryKeyHashFn(defaultedOptions) @@ -110,7 +125,10 @@ export class QueriesObserver extends Subscribable { this.notify() } - private onUpdate(observer: QueryObserver, result: QueryObserverResult): void { + private onUpdate( + observer: QueryObserver, + result: QueryObserverResult + ): void { const index = this.observers.indexOf(observer) if (index !== -1) { this.result = replaceAt(this.result, index, result) diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx index 2a0e6f8257..067e03fa61 100644 --- a/src/react/tests/useQueries.test.tsx +++ b/src/react/tests/useQueries.test.tsx @@ -1,7 +1,13 @@ import React from 'react' import { queryKey, renderWithClient, sleep } from './utils' -import { useQueries, QueryClient, UseQueryResult, QueryCache } from '../..' +import { + useQueries, + QueryClient, + UseQueryResult, + QueryCache, + QueryObserverResult, +} from '../..' describe('useQueries', () => { const queryCache = new QueryCache() @@ -30,4 +36,12 @@ describe('useQueries', () => { expect(results[1]).toMatchObject([{ data: 1 }, { data: undefined }]) expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) + + it('should flow through data types correctly (a type test; validated by successful compilation; not runtime results)', async () => { + const results: QueryObserverResult[] = useQueries( + [1, 2, 3].map(num => ({ queryKey: queryKey(), queryFn: () => num })) + ) + + expect(results.length).toBe(2) + }) }) diff --git a/src/react/useQueries.ts b/src/react/useQueries.ts index 81774b9ae8..3bb4301436 100644 --- a/src/react/useQueries.ts +++ b/src/react/useQueries.ts @@ -5,13 +5,22 @@ import { QueriesObserver } from '../core/queriesObserver' import { useQueryClient } from './QueryClientProvider' import { UseQueryOptions, UseQueryResult } from './types' -export function useQueries(queries: UseQueryOptions[]): UseQueryResult[] { +export function useQueries< + TQueryFnData = unknown, + TError = unknown, + TData = TQueryFnData +>( + queries: UseQueryOptions[] +): UseQueryResult[] { const queryClient = useQueryClient() // Create queries observer - const observerRef = React.useRef() + const observerRef = React.useRef< + QueriesObserver + >() const observer = - observerRef.current || new QueriesObserver(queryClient, queries) + observerRef.current || + new QueriesObserver(queryClient, queries) observerRef.current = observer // Update queries From 61a2a9a607d205bb8d9e90d777f45f34dc025edf Mon Sep 17 00:00:00 2001 From: John Reilly Date: Sat, 26 Dec 2020 19:46:56 +0000 Subject: [PATCH 2/7] fix: incorrect test --- src/react/tests/useQueries.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx index 067e03fa61..3d3ddc1d3b 100644 --- a/src/react/tests/useQueries.test.tsx +++ b/src/react/tests/useQueries.test.tsx @@ -42,6 +42,6 @@ describe('useQueries', () => { [1, 2, 3].map(num => ({ queryKey: queryKey(), queryFn: () => num })) ) - expect(results.length).toBe(2) + expect(results.length).toBe(3) }) }) From c16c002fd2cdcc9c7bf6ceebcc0d08b05d22012c Mon Sep 17 00:00:00 2001 From: johnnyreilly Date: Sat, 26 Dec 2020 20:08:53 +0000 Subject: [PATCH 3/7] fix: test no longer breaks rules of hooks --- src/react/tests/useQueries.test.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx index 3d3ddc1d3b..d60d4bba4b 100644 --- a/src/react/tests/useQueries.test.tsx +++ b/src/react/tests/useQueries.test.tsx @@ -38,10 +38,21 @@ describe('useQueries', () => { }) it('should flow through data types correctly (a type test; validated by successful compilation; not runtime results)', async () => { - const results: QueryObserverResult[] = useQueries( - [1, 2, 3].map(num => ({ queryKey: queryKey(), queryFn: () => num })) - ) + const key1 = queryKey() + const key2 = queryKey() + const results = []; - expect(results.length).toBe(3) + function Page() { + const result: QueryObserverResult[] = useQueries([ + { queryKey: key1, queryFn: () => 1 }, + { queryKey: key2, queryFn: () => 2 }, + ]) + results.push(...result); + return null + } + + renderWithClient(queryClient, ) + + await sleep(10) }) }) From bed7e0e736b82cc1d5ad205d56667afd18a9fccc Mon Sep 17 00:00:00 2001 From: johnnyreilly Date: Sun, 27 Dec 2020 15:22:29 +0000 Subject: [PATCH 4/7] feat: tests --- src/react/tests/useQueries.test.tsx | 38 +++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx index d60d4bba4b..179104bbb2 100644 --- a/src/react/tests/useQueries.test.tsx +++ b/src/react/tests/useQueries.test.tsx @@ -6,7 +6,6 @@ import { QueryClient, UseQueryResult, QueryCache, - QueryObserverResult, } from '../..' describe('useQueries', () => { @@ -37,17 +36,46 @@ describe('useQueries', () => { expect(results[2]).toMatchObject([{ data: 1 }, { data: 2 }]) }) - it('should flow through data types correctly (a type test; validated by successful compilation; not runtime results)', async () => { + it('should return same data types correctly (a type test; validated by successful compilation; not runtime results)', async () => { const key1 = queryKey() const key2 = queryKey() - const results = []; + const results: number[] = []; function Page() { - const result: QueryObserverResult[] = useQueries([ + const result = useQueries([ { queryKey: key1, queryFn: () => 1 }, { queryKey: key2, queryFn: () => 2 }, ]) - results.push(...result); + if (result[0].data) { + results.push(result[0].data) + } + if (result[1].data) { + results.push(result[1].data) + } + return null + } + + renderWithClient(queryClient, ) + + await sleep(10) + }) + + it('should return different data types correctly (a type test; validated by successful compilation; not runtime results)', async () => { + const key1 = queryKey() + const key2 = queryKey() + const results: (number | string)[] = []; + + function Page() { + const result = useQueries([ + { queryKey: key1, queryFn: () => 1 }, + { queryKey: key2, queryFn: () => 'two' }, + ]) + if (result[0].data) { + results.push(result[0].data) + } + if (result[1].data) { + results.push(result[1].data) + } return null } From 787c2ea1b6826297396cf24b2186cc2d57597db5 Mon Sep 17 00:00:00 2001 From: johnnyreilly Date: Wed, 6 Jan 2021 13:55:05 +0000 Subject: [PATCH 5/7] feat(types): now infers positional types --- docs/src/pages/reference/useQueries.md | 37 ++++++++++++++++++++++++++ src/react/tests/useQueries.test.tsx | 21 +++++++-------- src/react/useQueries.ts | 33 ++++++++++++++--------- 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/docs/src/pages/reference/useQueries.md b/docs/src/pages/reference/useQueries.md index c83a1261b6..d11da7e90c 100644 --- a/docs/src/pages/reference/useQueries.md +++ b/docs/src/pages/reference/useQueries.md @@ -19,3 +19,40 @@ The `useQueries` hook accepts an array with query option objects identical to th **Returns** The `useQueries` hook returns an array with all the query results. + +Proposed docs to add to the `useQueries` page: + +**TypeScript users** + +If you're using `react-query` with TypeScript then you'll be able to benefit from type inference: + +```ts +const resultWithAllTheSameTypes = useQueries( + [1, 2].map(x => ({ queryKey: `${x}`, queryFn: () => x })) +) +// resultWithAllTheSameTypes: QueryObserverResult[] + +const resultWithDifferentTypes = useQueries( + [1, 'two', new Date()].map(x => ({ queryKey: `${x}`, queryFn: () => x })) +) +// resultWithDifferentTypes: QueryObserverResult[] +``` + +In both the examples above, no types were specified and the compiler correctly inferred the types from the array passed to `useQueries`. + +However, if you pass an array literal *where different elements have a `queryFn` with differing return types* then the compiler will be able to correctly infer the positional types. Consider: + +```ts +const resultWithoutUsingMap = useQueries([ + { queryKey: key1, queryFn: () => 1 }, + { queryKey: key2, queryFn: () => 'two' }, +]) + +if (result[0].data) { + const isANumber: number = result[0].data +} +if (result[1].data) { + const isAString: string = result[1].data +} +``` + diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx index 179104bbb2..b53d86b7d8 100644 --- a/src/react/tests/useQueries.test.tsx +++ b/src/react/tests/useQueries.test.tsx @@ -1,12 +1,12 @@ import React from 'react' -import { queryKey, renderWithClient, sleep } from './utils' import { - useQueries, - QueryClient, - UseQueryResult, - QueryCache, -} from '../..' + expectType, + queryKey, + renderWithClient, + sleep, +} from './utils' +import { useQueries, QueryClient, UseQueryResult, QueryCache } from '../..' describe('useQueries', () => { const queryCache = new QueryCache() @@ -39,7 +39,7 @@ describe('useQueries', () => { it('should return same data types correctly (a type test; validated by successful compilation; not runtime results)', async () => { const key1 = queryKey() const key2 = queryKey() - const results: number[] = []; + const results: number[] = [] function Page() { const result = useQueries([ @@ -63,18 +63,17 @@ describe('useQueries', () => { it('should return different data types correctly (a type test; validated by successful compilation; not runtime results)', async () => { const key1 = queryKey() const key2 = queryKey() - const results: (number | string)[] = []; function Page() { - const result = useQueries([ + const result = useQueries([ { queryKey: key1, queryFn: () => 1 }, { queryKey: key2, queryFn: () => 'two' }, ]) if (result[0].data) { - results.push(result[0].data) + expectType(result[0].data) } if (result[1].data) { - results.push(result[1].data) + expectType(result[1].data) } return null } diff --git a/src/react/useQueries.ts b/src/react/useQueries.ts index 3bb4301436..0a8e24bf87 100644 --- a/src/react/useQueries.ts +++ b/src/react/useQueries.ts @@ -5,27 +5,34 @@ import { QueriesObserver } from '../core/queriesObserver' import { useQueryClient } from './QueryClientProvider' import { UseQueryOptions, UseQueryResult } from './types' -export function useQueries< - TQueryFnData = unknown, - TError = unknown, - TData = TQueryFnData ->( - queries: UseQueryOptions[] -): UseQueryResult[] { +type Awaited = T extends PromiseLike ? Awaited : T + +export function useQueries( + queries: [...TQueries] +): { + [ArrayElement in keyof TQueries]: UseQueryResult< + Awaited< + ReturnType< + NonNullable['queryFn']> + > + > + > +} { const queryClient = useQueryClient() // Create queries observer - const observerRef = React.useRef< - QueriesObserver - >() + const observerRef = React.useRef() const observer = observerRef.current || - new QueriesObserver(queryClient, queries) + new QueriesObserver( + queryClient, + queries as UseQueryOptions[] + ) observerRef.current = observer // Update queries if (observer.hasListeners()) { - observer.setQueries(queries) + observer.setQueries(queries as UseQueryOptions[]) } const [currentResult, setCurrentResult] = React.useState(() => @@ -38,5 +45,5 @@ export function useQueries< [observer] ) - return currentResult + return currentResult as any } From 5fd6006e62e22e95648a6558f449fc6f164123d4 Mon Sep 17 00:00:00 2001 From: johnnyreilly Date: Sun, 10 Jan 2021 17:01:15 +0000 Subject: [PATCH 6/7] fix(types): if select is provided then type is unknown --- src/react/tests/useQueries.test.tsx | 37 ++++++++++++++++++++++++----- src/react/useQueries.ts | 14 +++++++---- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx index b53d86b7d8..d40f19a267 100644 --- a/src/react/tests/useQueries.test.tsx +++ b/src/react/tests/useQueries.test.tsx @@ -1,11 +1,6 @@ import React from 'react' -import { - expectType, - queryKey, - renderWithClient, - sleep, -} from './utils' +import { expectType, queryKey, renderWithClient, sleep } from './utils' import { useQueries, QueryClient, UseQueryResult, QueryCache } from '../..' describe('useQueries', () => { @@ -82,4 +77,34 @@ describe('useQueries', () => { await sleep(10) }) + + it('if select is provided then the return type should be unknown (a type test; validated by successful compilation; not runtime results)', async () => { + const key1 = queryKey() + const key2 = queryKey() + + function Page() { + const result = useQueries([ + { + queryKey: key1, + queryFn: () => ({ prop: 'value' }), + // here x is unknown; we use x.prop without testing - triggering `Object is of type 'unknown'.ts(2571)` + // @ts-expect-error + select: x => x.prop, + }, + { queryKey: key2, queryFn: () => 1 }, + ]) + + if (result[0].data) { + expectType(result[0].data) + } + if (result[1].data) { + expectType(result[1].data) + } + return null + } + + renderWithClient(queryClient, ) + + await sleep(10) + }) }) diff --git a/src/react/useQueries.ts b/src/react/useQueries.ts index 0a8e24bf87..7c2409c5b1 100644 --- a/src/react/useQueries.ts +++ b/src/react/useQueries.ts @@ -11,11 +11,15 @@ export function useQueries( queries: [...TQueries] ): { [ArrayElement in keyof TQueries]: UseQueryResult< - Awaited< - ReturnType< - NonNullable['queryFn']> - > - > + TQueries[ArrayElement] extends { select: any } + ? unknown + : Awaited< + ReturnType< + NonNullable< + Extract['queryFn'] + > + > + > > } { const queryClient = useQueryClient() From 7af14b7da7c799cfb93eb2b3bb2c59b512c7881e Mon Sep 17 00:00:00 2001 From: johnnyreilly Date: Sun, 10 Jan 2021 17:33:38 +0000 Subject: [PATCH 7/7] fix(types): if select is provided then flow that return type through --- src/react/tests/useQueries.test.tsx | 17 +++++++++++++---- src/react/useQueries.ts | 6 ++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/react/tests/useQueries.test.tsx b/src/react/tests/useQueries.test.tsx index d40f19a267..9a277bbded 100644 --- a/src/react/tests/useQueries.test.tsx +++ b/src/react/tests/useQueries.test.tsx @@ -78,27 +78,36 @@ describe('useQueries', () => { await sleep(10) }) - it('if select is provided then the return type should be unknown (a type test; validated by successful compilation; not runtime results)', async () => { + it('if select is provided then the return type should flow from that (a type test; validated by successful compilation; not runtime results)', async () => { const key1 = queryKey() const key2 = queryKey() + const key3 = queryKey() function Page() { const result = useQueries([ { queryKey: key1, queryFn: () => ({ prop: 'value' }), + select: x => x, + }, + { + queryKey: key2, + queryFn: () => ({ prop: 'value' }), // here x is unknown; we use x.prop without testing - triggering `Object is of type 'unknown'.ts(2571)` // @ts-expect-error - select: x => x.prop, + select: x => x.prop as string, }, - { queryKey: key2, queryFn: () => 1 }, + { queryKey: key3, queryFn: () => 1 }, ]) if (result[0].data) { expectType(result[0].data) } if (result[1].data) { - expectType(result[1].data) + expectType(result[1].data) + } + if (result[2].data) { + expectType(result[2].data) } return null } diff --git a/src/react/useQueries.ts b/src/react/useQueries.ts index 7c2409c5b1..b138a0e8a6 100644 --- a/src/react/useQueries.ts +++ b/src/react/useQueries.ts @@ -11,8 +11,10 @@ export function useQueries( queries: [...TQueries] ): { [ArrayElement in keyof TQueries]: UseQueryResult< - TQueries[ArrayElement] extends { select: any } - ? unknown + TQueries[ArrayElement] extends { select: infer TSelect } + ? TSelect extends (data: any) => any + ? ReturnType + : never : Awaited< ReturnType< NonNullable<