Skip to content

Commit

Permalink
fix(types): remove non-void constraint from queryFn result (#3666)
Browse files Browse the repository at this point in the history
* fix(types): remove non-void constraint from queryFn result

* test(types): remove non-void tests, and add tests for handling fetch -> promise<any>

* test(types): remove ts-expect-error from query test file
  • Loading branch information
artysidorenko authored Jun 3, 2022
1 parent 8095859 commit f77f6f4
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 104 deletions.
1 change: 0 additions & 1 deletion src/core/tests/query.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -810,7 +810,6 @@ describe('query', () => {

const observer = new QueryObserver(queryClient, {
queryKey: key,
// @ts-expect-error (queryFn must not return undefined)
queryFn: () => undefined,
retry: false,
})
Expand Down
8 changes: 1 addition & 7 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ export type QueryKey = readonly unknown[]
export type QueryFunction<
T = unknown,
TQueryKey extends QueryKey = QueryKey
> = (
context: QueryFunctionContext<TQueryKey>
) => [T] extends [undefined]
? never | 'queryFn must not return undefined or void'
: [T] extends [void]
? never | 'queryFn must not return undefined or void'
: T | Promise<T>
> = (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>

export interface QueryFunctionContext<
TQueryKey extends QueryKey = QueryKey,
Expand Down
101 changes: 31 additions & 70 deletions src/reactjs/tests/useQueries.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -607,76 +607,6 @@ 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<void>)
queries: [{ queryKey: key1, queryFn: (): Promise<void> => undefined }],
})

useQueries({
queries: [
// @ts-expect-error (queryFn must not return explicitly Promise<undefined>)
{ queryKey: key1, queryFn: (): Promise<undefined> => undefined },
],
})
useQueries({
queries: [
// @ts-expect-error (queryFn must not return Promise<undefined>)
{ queryKey: key2, queryFn: () => Promise.resolve(undefined) },
],
})
useQueries({
// @ts-expect-error (queryFn must not return Promise<undefined>)
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) => ({
Expand Down Expand Up @@ -898,6 +828,37 @@ describe('useQueries', () => {
someInvalidField: '',
})),
})

// field names should be enforced - array literal
useQueries({
queries: [
{
queryKey: key1,
queryFn: () => 'string',
// @ts-expect-error (invalidField)
someInvalidField: [],
},
],
})

// supports queryFn using fetch() to return Promise<any> - Array.map() result
useQueries({
queries: Array(50).map((_, i) => ({
queryKey: ['key', i] as const,
queryFn: () => fetch('return Promise<any>').then(resp => resp.json()),
})),
})

// supports queryFn using fetch() to return Promise<any> - array literal
useQueries({
queries: [
{
queryKey: key1,
queryFn: () =>
fetch('return Promise<any>').then(resp => resp.json()),
},
],
})
}
})

Expand Down
60 changes: 44 additions & 16 deletions src/reactjs/tests/useQuery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
QueryCache,
QueryFunction,
QueryFunctionContext,
UseQueryOptions,
} from '../..'
import { ErrorBoundary } from 'react-error-boundary'

Expand All @@ -36,22 +37,6 @@ describe('useQuery', () => {
expectType<unknown>(noQueryFn.data)
expectType<unknown>(noQueryFn.error)

// it should not be possible for queryFn to return undefined
// @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<void> return type
// @ts-expect-error (queryFn explicit return type is Promise<void>)
useQuery(key, (): Promise<void> => Promise.resolve())

// it should not be possible for queryFn to have explicit Promise<undefined> return type
// @ts-expect-error (queryFn explicit return type is Promise<undefined>)
useQuery(key, (): Promise<undefined> => Promise.resolve(undefined))

// it should infer the result type from the query function
const fromQueryFn = useQuery(key, () => 'test')
expectType<string | undefined>(fromQueryFn.data)
Expand Down Expand Up @@ -137,6 +122,49 @@ describe('useQuery', () => {
queryKey: ['1'],
queryFn: getMyDataStringKey,
})

// it should handle query-functions that return Promise<any>
useQuery(key, () =>
fetch('return Promise<any>').then(resp => resp.json())
)

// handles wrapped queries with custom fetcher passed as inline queryFn
const useWrappedQuery = <
TQueryKey extends [string, Record<string, unknown>?],
TQueryFnData,
TError,
TData = TQueryFnData
>(
qk: TQueryKey,
fetcher: (
obj: TQueryKey[1],
token: string
// return type must be wrapped with TQueryFnReturn
) => Promise<TQueryFnData>,
options?: Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn'
>
) => useQuery(qk, () => fetcher(qk[1], 'token'), options)
const test = useWrappedQuery([''], async () => '1')
expectType<string | undefined>(test.data)

// handles wrapped queries with custom fetcher passed directly to useQuery
const useWrappedFuncStyleQuery = <
TQueryKey extends [string, Record<string, unknown>?],
TQueryFnData,
TError,
TData = TQueryFnData
>(
qk: TQueryKey,
fetcher: () => Promise<TQueryFnData>,
options?: Omit<
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
'queryKey' | 'queryFn'
>
) => useQuery(qk, fetcher, options)
const testFuncStyle = useWrappedFuncStyleQuery([''], async () => true)
expectType<boolean | undefined>(testFuncStyle.data)
}
})

Expand Down
12 changes: 2 additions & 10 deletions src/reactjs/useQueries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ type UseQueryOptionsForUseQueries<
TQueryKey extends QueryKey = QueryKey
> = Omit<UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>, 'context'>

type InvalidQueryFn = QueryFunction<
undefined | Promise<undefined> | void | Promise<void>
>

// Avoid TS depth-limit error in case of large array literal
type MAXIMUM_DEPTH = 20

Expand All @@ -44,9 +40,7 @@ type GetOptions<T> =
: T extends [infer TQueryFnData]
? UseQueryOptionsForUseQueries<TQueryFnData>
: // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided
T extends { queryFn?: InvalidQueryFn }
? never | 'queryFn must not return undefined or void'
: T extends {
T extends {
queryFn?: QueryFunction<infer TQueryFnData, infer TQueryKey>
select: (data: any) => infer TData
}
Expand Down Expand Up @@ -106,9 +100,7 @@ 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 { queryFn: InvalidQueryFn }[]
? (never | 'queryFn must not return undefined or void')[]
: T extends UseQueryOptionsForUseQueries<
T extends UseQueryOptionsForUseQueries<
infer TQueryFnData,
infer TError,
infer TData,
Expand Down

0 comments on commit f77f6f4

Please sign in to comment.