import { describe, expectTypeOf, it } from 'vitest' import { queryKey } from '@tanstack/query-test-utils' import { skipToken } from '..' import { useQueries } from '../useQueries' import { queryOptions } from '../queryOptions' import type { OmitKeyof, QueryFunction, QueryKey } from '..' import type { UseQueryOptions, UseQueryResult } from '../types' import type { QueryFunctionContext } from '@tanstack/query-core' describe('useQueries', () => { describe('config object overload', () => { it('TData should always be defined when initialData is provided as an object', () => { const query1 = { queryKey: queryKey(), queryFn: () => { return { wow: true, } }, initialData: { wow: false, }, } const query2 = { queryKey: queryKey(), queryFn: () => 'Query Data', initialData: 'initial data', } const query3 = { queryKey: queryKey(), queryFn: () => 'Query Data', } const queryResults = useQueries({ queries: [query1, query2, query3] }) const query1Data = queryResults[0].data const query2Data = queryResults[1].data const query3Data = queryResults[2].data expectTypeOf(query1Data).toEqualTypeOf<{ wow: boolean }>() expectTypeOf(query2Data).toEqualTypeOf() expectTypeOf(query3Data).toEqualTypeOf() }) it('TData should be defined when passed through queryOptions', () => { const options = queryOptions({ queryKey: queryKey(), queryFn: () => { return { wow: true, } }, initialData: { wow: true, }, }) const queryResults = useQueries({ queries: [options] }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) it('should be possible to define a different TData than TQueryFnData using select with queryOptions spread into useQueries', () => { const query1 = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), select: (data) => data > 1, }) const query2 = { queryKey: queryKey(), queryFn: () => Promise.resolve(1), select: (data: number) => data > 1, } const queryResults = useQueries({ queries: [query1, query2] }) const query1Data = queryResults[0].data const query2Data = queryResults[1].data expectTypeOf(query1Data).toEqualTypeOf() expectTypeOf(query2Data).toEqualTypeOf() }) it('TData should have undefined in the union when initialData is provided as a function which can return undefined', () => { const queryResults = useQueries({ queries: [ { queryKey: queryKey(), queryFn: () => { return { wow: true, } }, initialData: () => undefined as { wow: boolean } | undefined, }, ], }) const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf<{ wow: boolean } | undefined>() }) describe('custom hook', () => { it('should allow custom hooks using UseQueryOptions', () => { type Data = string const useCustomQueries = ( options?: OmitKeyof, 'queryKey' | 'queryFn'>, ) => { return useQueries({ queries: [ { ...options, queryKey: queryKey(), queryFn: () => Promise.resolve('data'), }, ], }) } const queryResults = useCustomQueries() const data = queryResults[0].data expectTypeOf(data).toEqualTypeOf() }) }) it('TData should have correct type when conditional skipToken is passed', () => { const queryResults = useQueries({ queries: [ { queryKey: queryKey(), queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), }, ], }) const firstResult = queryResults[0] expectTypeOf(firstResult).toEqualTypeOf>() expectTypeOf(firstResult.data).toEqualTypeOf() }) it('should return correct data for dynamic queries with mixed result types', () => { const Queries1 = { get: () => queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), }), } const Queries2 = { get: () => queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(true), }), } const queries1List = [1, 2, 3].map(() => ({ ...Queries1.get() })) const result = useQueries({ queries: [...queries1List, { ...Queries2.get() }], }) expectTypeOf(result).toEqualTypeOf< [ ...Array>, UseQueryResult, ] >() }) }) describe('type parameters', () => { it('should handle type parameter - tuple of tuples', () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() // @ts-expect-error (Page component is not rendered) function Page() { const result1 = useQueries< [[number], [string], [Array, boolean]] >({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }) expectTypeOf(result1[0]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result1[1]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result1[2]).toEqualTypeOf< UseQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // TData (3rd element) takes precedence over TQueryFnData (1st element) const result2 = useQueries< [[string, unknown, string], [string, unknown, number]] >({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }) expectTypeOf(result2[0]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result2[1]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // types should be enforced useQueries<[[string, unknown, string], [string, boolean, number]]>({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, ], }) // field names should be enforced useQueries<[[string]]>({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) } }) it('should handle type parameter - tuple of objects', () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() // @ts-expect-error (Page component is not rendered) function Page() { const result1 = useQueries< [ { queryFnData: number }, { queryFnData: string }, { queryFnData: Array; error: boolean }, ] >({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], }, ], }) expectTypeOf(result1[0]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result1[1]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result1[2]).toEqualTypeOf< UseQueryResult, boolean> >() expectTypeOf(result1[0].data).toEqualTypeOf() expectTypeOf(result1[1].data).toEqualTypeOf() expectTypeOf(result1[2].data).toEqualTypeOf | undefined>() expectTypeOf(result1[2].error).toEqualTypeOf() // TData (data prop) takes precedence over TQueryFnData (queryFnData prop) const result2 = useQueries< [ { queryFnData: string; data: string }, { queryFnData: string; data: number }, ] >({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }, ], }) expectTypeOf(result2[0]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result2[1]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result2[0].data).toEqualTypeOf() expectTypeOf(result2[1].data).toEqualTypeOf() // can pass only TData (data prop) although TQueryFnData will be left unknown const result3 = useQueries<[{ data: string }, { data: number }]>({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a as string }, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a as number }, }, ], }) expectTypeOf(result3[0]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result3[1]).toEqualTypeOf< UseQueryResult >() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() // types should be enforced useQueries< [ { queryFnData: string; data: string }, { queryFnData: string; data: number; error: boolean }, ] >({ queries: [ { queryKey: key1, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, ], }) // field names should be enforced useQueries<[{ queryFnData: string }]>({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) } }) it('should return correct types when passing through queryOptions', () => { // @ts-expect-error (Page component is not rendered) function Page() { // data and results types are correct when using queryOptions const result4 = useQueries({ queries: [ queryOptions({ queryKey: queryKey(), queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return a.toLowerCase() }, }), queryOptions({ queryKey: queryKey(), queryFn: () => 'string', select: (a) => { expectTypeOf(a).toEqualTypeOf() return parseInt(a) }, }), ], }) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[0].data).toEqualTypeOf() expectTypeOf(result4[1].data).toEqualTypeOf() } }) it('should handle array literal without type parameter to infer result type', () => { const key1 = queryKey() const key2 = queryKey() const key3 = queryKey() const key4 = queryKey() const key5 = queryKey() type BizError = { code: number } const throwOnError = (_error: BizError) => true // @ts-expect-error (Page component is not rendered) function Page() { // Array.map preserves TQueryFnData const result1 = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, })), }) expectTypeOf(result1).toEqualTypeOf< Array> >() if (result1[0]) { expectTypeOf(result1[0].data).toEqualTypeOf() } // Array.map preserves TError const result1_err = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, throwOnError, })), }) expectTypeOf(result1_err).toEqualTypeOf< Array> >() if (result1_err[0]) { expectTypeOf(result1_err[0].data).toEqualTypeOf() expectTypeOf(result1_err[0].error).toEqualTypeOf() } // Array.map preserves TData const result2 = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), })), }) expectTypeOf(result2).toEqualTypeOf< Array> >() const result2_err = useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), throwOnError, })), }) expectTypeOf(result2_err).toEqualTypeOf< Array> >() const result3 = useQueries({ queries: [ { queryKey: key1, queryFn: () => 1, }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key3, queryFn: () => ['string[]'], select: () => 123, }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], }) expectTypeOf(result3[0]).toEqualTypeOf>() expectTypeOf(result3[1]).toEqualTypeOf>() expectTypeOf(result3[2]).toEqualTypeOf>() expectTypeOf(result3[0].data).toEqualTypeOf() expectTypeOf(result3[1].data).toEqualTypeOf() expectTypeOf(result3[3].data).toEqualTypeOf() // select takes precedence over queryFn expectTypeOf(result3[2].data).toEqualTypeOf() // infer TError from throwOnError expectTypeOf(result3[3].error).toEqualTypeOf() // initialData/placeholderData are enforced useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', placeholderData: 'string', // @ts-expect-error (initialData: string) initialData: 123, }, { queryKey: key2, queryFn: () => 123, // @ts-expect-error (placeholderData: number) placeholderData: 'string', initialData: 123, }, ], }) // select and throwOnError params are "indirectly" enforced useQueries({ queries: [ // unfortunately TS will not suggest the type for you { queryKey: key1, queryFn: () => 'string', }, // however you can add a type to the callback { queryKey: key2, queryFn: () => 'string', }, // the type you do pass is enforced { queryKey: key3, queryFn: () => 'string', }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], }) // callbacks are also indirectly enforced with Array.map useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), })), }) useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => i + 10, select: (data: number) => data.toString(), })), }) // results inference works when all the handlers are defined const result4 = useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', }, { queryKey: key2, queryFn: () => 'string', }, { queryKey: key4, queryFn: () => 'string', select: (a: string) => parseInt(a), }, { queryKey: key5, queryFn: () => 'string', select: (a: string) => parseInt(a), throwOnError, }, ], }) expectTypeOf(result4[0]).toEqualTypeOf>() expectTypeOf(result4[1]).toEqualTypeOf>() expectTypeOf(result4[2]).toEqualTypeOf>() expectTypeOf(result4[3]).toEqualTypeOf< UseQueryResult >() // handles when queryFn returns a Promise const result5 = useQueries({ queries: [ { queryKey: key1, queryFn: () => Promise.resolve('string'), }, ], }) expectTypeOf(result5[0]).toEqualTypeOf>() // Array as const does not throw error const result6 = useQueries({ queries: [ { queryKey: ['key1'], queryFn: () => 'string', }, { queryKey: ['key1'], queryFn: () => 123, }, { queryKey: key5, queryFn: () => 'string', throwOnError, }, ], } as const) expectTypeOf(result6[0]).toEqualTypeOf>() expectTypeOf(result6[1]).toEqualTypeOf>() expectTypeOf(result6[2]).toEqualTypeOf< UseQueryResult >() // field names should be enforced - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => 'string', // @ts-expect-error (invalidField) someInvalidField: [], }, ], }) // field names should be enforced - Array.map() result useQueries({ // @ts-expect-error (invalidField) queries: Array(10).map(() => ({ 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 - Array.map() result useQueries({ queries: Array(50).map((_, i) => ({ queryKey: ['key', i] as const, queryFn: () => fetch('return Promise').then((resp) => resp.json()), })), }) // supports queryFn using fetch() to return Promise - array literal useQueries({ queries: [ { queryKey: key1, queryFn: () => fetch('return Promise').then((resp) => resp.json()), }, ], }) } }) it('should handle strongly typed queryFn factories and useQueries wrappers', () => { // QueryKey + queryFn factory type QueryKeyA = ['queryA'] const getQueryKeyA = (): QueryKeyA => ['queryA'] type GetQueryFunctionA = () => QueryFunction const getQueryFunctionA: GetQueryFunctionA = () => () => { return Promise.resolve(1) } type SelectorA = (data: number) => [number, string] const getSelectorA = (): SelectorA => (data) => [data, data.toString()] type QueryKeyB = ['queryB', string] const getQueryKeyB = (id: string): QueryKeyB => ['queryB', id] type GetQueryFunctionB = () => QueryFunction const getQueryFunctionB: GetQueryFunctionB = () => () => { return Promise.resolve('1') } type SelectorB = (data: string) => [string, number] const getSelectorB = (): SelectorB => (data) => [data, +data] // Wrapper with strongly typed array-parameter function useWrappedQueries< TQueryFnData, TError, TData, TQueryKey extends QueryKey, >( queries: Array>, ) { return useQueries({ queries: queries.map( // no need to type the mapped query (query) => { const { queryFn: fn, queryKey: key } = query expectTypeOf(fn).toEqualTypeOf< | typeof skipToken | QueryFunction | undefined >() return { queryKey: key, queryFn: fn && fn !== skipToken ? (ctx: QueryFunctionContext) => { // eslint-disable-next-line vitest/valid-expect expectTypeOf(ctx.queryKey) return fn.call({}, ctx) } : undefined, } }, ), }) } // @ts-expect-error (Page component is not rendered) function Page() { const result = useQueries({ queries: [ { queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), }, { queryKey: getQueryKeyB('id'), queryFn: getQueryFunctionB(), }, ], }) expectTypeOf(result[0]).toEqualTypeOf>() expectTypeOf(result[1]).toEqualTypeOf>() const withSelector = useQueries({ queries: [ { queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), select: getSelectorA(), }, { queryKey: getQueryKeyB('id'), queryFn: getQueryFunctionB(), select: getSelectorB(), }, ], }) expectTypeOf(withSelector[0]).toEqualTypeOf< UseQueryResult<[number, string], Error> >() expectTypeOf(withSelector[1]).toEqualTypeOf< UseQueryResult<[string, number], Error> >() const withWrappedQueries = useWrappedQueries( Array(10).map(() => ({ queryKey: getQueryKeyA(), queryFn: getQueryFunctionA(), select: getSelectorA(), })), ) expectTypeOf(withWrappedQueries).toEqualTypeOf< Array> >() } }) }) describe('select', () => { // Inferring the `select` argument of an *inline* query object from its // sibling `queryFn` is a known TypeScript limitation, because `useQueries` // infers its array generic from the argument itself. The two supported // workarounds are to annotate the `select` parameter, or to define the // query with the `queryOptions` helper. // https://github.com/TanStack/query/issues/6556 describe('without queryOptions (inline query object)', () => { it('leaves the select argument as `unknown` without an annotation', () => { useQueries({ queries: [ { queryKey: queryKey(), queryFn: () => Promise.resolve(1), select: (data) => { expectTypeOf(data).toBeUnknown() // @ts-expect-error `data` is `unknown`, not the expected `number` return data.toFixed() }, }, ], }) }) it('infers the result when the select parameter is annotated', () => { const queryResults = useQueries({ queries: [ { queryKey: queryKey(), queryFn: () => Promise.resolve(1), select: (data: number) => data.toFixed(), }, ], }) expectTypeOf(queryResults[0].data).toEqualTypeOf() }) }) describe('with queryOptions passed directly', () => { it('without select, infers the queryFn data as the result', () => { const options = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), }) const queryResults = useQueries({ queries: [options] }) expectTypeOf(queryResults[0].data).toEqualTypeOf() }) it('with select, infers the select argument and the result', () => { const options = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), select: (data) => { expectTypeOf(data).toEqualTypeOf() return data.toFixed() }, }) const queryResults = useQueries({ queries: [options] }) expectTypeOf(queryResults[0].data).toEqualTypeOf() }) it('infers select when a base queryOptions is re-wrapped with queryOptions', () => { const baseOptions = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), }) const queryResults = useQueries({ queries: [ queryOptions({ ...baseOptions, select: (data) => { expectTypeOf(data).toEqualTypeOf() return data.toFixed() }, }), baseOptions, ], }) expectTypeOf(queryResults[0].data).toEqualTypeOf() expectTypeOf(queryResults[1].data).toEqualTypeOf() }) it('infers an overriding select when a queryOptions with a select is re-wrapped with queryOptions', () => { const baseOptions = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), select: (data) => data + 1, }) const queryResults = useQueries({ queries: [ queryOptions({ ...baseOptions, select: (data) => { expectTypeOf(data).toEqualTypeOf() return data.toFixed() }, }), ], }) expectTypeOf(queryResults[0].data).toEqualTypeOf() }) }) describe('with queryOptions spread into an inline query object', () => { it('without select in the factory, leaves an unannotated select as `unknown`', () => { const options = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), }) useQueries({ queries: [ // @ts-expect-error Without an annotation the inline `select` receives `data: unknown`, which makes the whole spread query object unassignable to the expected options type { ...options, select: (data) => { expectTypeOf(data).toBeUnknown() return data }, }, ], }) }) it('without select in the factory, an annotated select compiles', () => { const options = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), }) const queryResults = useQueries({ queries: [{ ...options, select: (data: number) => data.toFixed() }], }) expectTypeOf(queryResults[0].data).toEqualTypeOf() }) it('with select in the factory, leaves an unannotated overriding select as `unknown`', () => { const options = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), select: (data) => data + 1, }) useQueries({ queries: [ // @ts-expect-error Without an annotation the inline `select` receives `data: unknown`, which makes the whole spread query object unassignable to the expected options type { ...options, select: (data) => { expectTypeOf(data).toBeUnknown() return data }, }, ], }) }) it('with select in the factory, an annotated overriding select compiles', () => { const options = queryOptions({ queryKey: queryKey(), queryFn: () => Promise.resolve(1), select: (data) => data + 1, }) const queryResults = useQueries({ queries: [{ ...options, select: (data: number) => data.toFixed() }], }) expectTypeOf(queryResults[0].data).toEqualTypeOf() }) }) }) })