From 837bb0ef4aa159bf4c238c1f204cd77f8a4ba506 Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Sat, 20 Jul 2024 20:19:49 +1000 Subject: [PATCH 1/2] Fix createQueries initialData type --- .../src/__tests__/queryOptions.test-d.tsx | 14 ++--- packages/svelte-query/src/createQueries.ts | 59 +++++++++++------- packages/svelte-query/src/queryOptions.ts | 8 +-- .../createQueries/createQueries.test-d.ts | 24 ++++++++ .../infiniteQueryOptions.test-d.ts | 60 +++++++++++++++++++ .../infiniteQueryOptions.test.ts | 28 --------- ...Options.test.ts => queryOptions.test-d.ts} | 37 +++++++++++- 7 files changed, 168 insertions(+), 62 deletions(-) create mode 100644 packages/svelte-query/tests/createQueries/createQueries.test-d.ts create mode 100644 packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test-d.ts delete mode 100644 packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test.ts rename packages/svelte-query/tests/queryOptions/{queryOptions.test.ts => queryOptions.test-d.ts} (81%) diff --git a/packages/react-query/src/__tests__/queryOptions.test-d.tsx b/packages/react-query/src/__tests__/queryOptions.test-d.tsx index 242be3972c..f019d883b3 100644 --- a/packages/react-query/src/__tests__/queryOptions.test-d.tsx +++ b/packages/react-query/src/__tests__/queryOptions.test-d.tsx @@ -1,4 +1,4 @@ -import { describe, expect, expectTypeOf, it } from 'vitest' +import { describe, expectTypeOf, it } from 'vitest' import { QueriesObserver, QueryClient, @@ -71,14 +71,12 @@ describe('queryOptions', () => { expectTypeOf(data).toEqualTypeOf() }) it('should tag the queryKey with the result type of the QueryFn', () => { - expect(() => { - const { queryKey } = queryOptions({ - queryKey: ['key'], - queryFn: () => Promise.resolve(5), - }) - - expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() + const { queryKey } = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), }) + + expectTypeOf(queryKey[dataTagSymbol]).toEqualTypeOf() }) it('should tag the queryKey even if no promise is returned', () => { const { queryKey } = queryOptions({ diff --git a/packages/svelte-query/src/createQueries.ts b/packages/svelte-query/src/createQueries.ts index 6e16ec40af..cb1ba75679 100644 --- a/packages/svelte-query/src/createQueries.ts +++ b/packages/svelte-query/src/createQueries.ts @@ -7,6 +7,7 @@ import type { Readable } from 'svelte/store' import type { StoreOrVal } from './types' import type { DefaultError, + DefinedQueryObserverResult, OmitKeyof, QueriesObserverOptions, QueriesPlaceholderDataFunction, @@ -19,7 +20,7 @@ import type { } from '@tanstack/query-core' // This defines the `CreateQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. -// `placeholderData` function does not have a parameter +// `placeholderData` function always gets undefined passed type QueryObserverOptionsForCreateQueries< TQueryFnData = unknown, TError = DefaultError, @@ -38,7 +39,7 @@ type MAXIMUM_DEPTH = 20 // Widen the type of the symbol to enable type inference even if skipToken is not immutable. type SkipTokenForUseQueries = symbol -type GetOptions = +type GetQueryObserverOptionsForCreateQueries = // Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData } T extends { queryFnData: infer TQueryFnData @@ -74,21 +75,38 @@ type GetOptions = : // Fallback QueryObserverOptionsForCreateQueries -type GetResults = +// A defined initialData setting should return a DefinedQueryObserverResult rather than CreateQueryResult +type GetDefinedOrUndefinedQueryResult = T extends { + initialData?: infer TInitialData +} + ? unknown extends TInitialData + ? QueryObserverResult + : TInitialData extends TData + ? DefinedQueryObserverResult + : TInitialData extends () => infer TInitialDataResult + ? unknown extends TInitialDataResult + ? QueryObserverResult + : TInitialDataResult extends TData + ? DefinedQueryObserverResult + : QueryObserverResult + : QueryObserverResult + : QueryObserverResult + +type GetCreateQueryResult = // Part 1: responsible for mapping explicit type parameter to function result, if object T extends { queryFnData: any; error?: infer TError; data: infer TData } - ? QueryObserverResult + ? GetDefinedOrUndefinedQueryResult : T extends { queryFnData: infer TQueryFnData; error?: infer TError } - ? QueryObserverResult + ? GetDefinedOrUndefinedQueryResult : T extends { data: infer TData; error?: infer TError } - ? QueryObserverResult + ? GetDefinedOrUndefinedQueryResult : // Part 2: responsible for mapping explicit type parameter to function result, if tuple T extends [any, infer TError, infer TData] - ? QueryObserverResult + ? GetDefinedOrUndefinedQueryResult : T extends [infer TQueryFnData, infer TError] - ? QueryObserverResult + ? GetDefinedOrUndefinedQueryResult : T extends [infer TQueryFnData] - ? QueryObserverResult + ? GetDefinedOrUndefinedQueryResult : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided T extends { queryFn?: @@ -97,7 +115,8 @@ type GetResults = select?: (data: any) => infer TData throwOnError?: ThrowOnError } - ? QueryObserverResult< + ? GetDefinedOrUndefinedQueryResult< + T, unknown extends TData ? TQueryFnData : TData, unknown extends TError ? DefaultError : TError > @@ -109,18 +128,18 @@ type GetResults = */ export type QueriesOptions< T extends Array, - TResult extends Array = [], + TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] - ? [...TResult, GetOptions] - : T extends [infer Head, ...infer Tail] + ? [...TResults, GetQueryObserverOptionsForCreateQueries] + : T extends [infer Head, ...infer Tails] ? QueriesOptions< - [...Tail], - [...TResult, GetOptions], + [...Tails], + [...TResults, GetQueryObserverOptionsForCreateQueries], [...TDepth, 1] > : ReadonlyArray extends T @@ -151,18 +170,18 @@ export type QueriesOptions< */ export type QueriesResults< T extends Array, - TResult extends Array = [], + TResults extends Array = [], TDepth extends ReadonlyArray = [], > = TDepth['length'] extends MAXIMUM_DEPTH ? Array : T extends [] ? [] : T extends [infer Head] - ? [...TResult, GetResults] - : T extends [infer Head, ...infer Tail] + ? [...TResults, GetCreateQueryResult] + : T extends [infer Head, ...infer Tails] ? QueriesResults< - [...Tail], - [...TResult, GetResults], + [...Tails], + [...TResults, GetCreateQueryResult], [...TDepth, 1] > : T extends Array< diff --git a/packages/svelte-query/src/queryOptions.ts b/packages/svelte-query/src/queryOptions.ts index 7e958f3df7..94c75e8056 100644 --- a/packages/svelte-query/src/queryOptions.ts +++ b/packages/svelte-query/src/queryOptions.ts @@ -29,8 +29,8 @@ export function queryOptions< TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( - options: UndefinedInitialDataOptions, -): UndefinedInitialDataOptions & { + options: DefinedInitialDataOptions, +): DefinedInitialDataOptions & { queryKey: DataTag } @@ -40,8 +40,8 @@ export function queryOptions< TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, >( - options: DefinedInitialDataOptions, -): DefinedInitialDataOptions & { + options: UndefinedInitialDataOptions, +): UndefinedInitialDataOptions & { queryKey: DataTag } diff --git a/packages/svelte-query/tests/createQueries/createQueries.test-d.ts b/packages/svelte-query/tests/createQueries/createQueries.test-d.ts new file mode 100644 index 0000000000..32721b2f49 --- /dev/null +++ b/packages/svelte-query/tests/createQueries/createQueries.test-d.ts @@ -0,0 +1,24 @@ +import { describe, expectTypeOf, test } from 'vitest' +import { get } from 'svelte/store' +import { createQueries, queryOptions } from '../../src/index' + +describe('createQueries', () => { + test('TData should be defined when passed through queryOptions', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => { + return { + wow: true, + } + }, + initialData: { + wow: true, + }, + }) + const queryResults = createQueries({ queries: [options] }) + + const data = get(queryResults)[0].data + + expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() + }) +}) diff --git a/packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test-d.ts b/packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test-d.ts new file mode 100644 index 0000000000..0b4af0992e --- /dev/null +++ b/packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test-d.ts @@ -0,0 +1,60 @@ +import { describe, expectTypeOf, test } from 'vitest' +import { get } from 'svelte/store' +import { QueryClient } from '@tanstack/query-core' +import { createInfiniteQuery, infiniteQueryOptions } from '../../src/index' +import type { InfiniteData } from '@tanstack/query-core' + +describe('queryOptions', () => { + test('Should not allow excess properties', () => { + infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('data'), + getNextPageParam: () => 1, + initialPageParam: 1, + // @ts-expect-error this is a good error, because stallTime does not exist! + stallTime: 1000, + }) + }) + + test('Should infer types for callbacks', () => { + infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('data'), + staleTime: 1000, + getNextPageParam: () => 1, + initialPageParam: 1, + select: (data) => { + expectTypeOf(data).toEqualTypeOf>() + }, + }) + }) + + test('Should work when passed to createInfiniteQuery', () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + }) + + const query = createInfiniteQuery(options) + + // known issue: type of pageParams is unknown when returned from useInfiniteQuery + expectTypeOf(get(query).data).toEqualTypeOf< + InfiniteData | undefined + >() + }) + + test('Should work when passed to fetchInfiniteQuery', async () => { + const options = infiniteQueryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve('string'), + getNextPageParam: () => 1, + initialPageParam: 1, + }) + + const data = await new QueryClient().fetchInfiniteQuery(options) + + expectTypeOf(data).toEqualTypeOf>() + }) +}) diff --git a/packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test.ts b/packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test.ts deleted file mode 100644 index 18b048f2c9..0000000000 --- a/packages/svelte-query/tests/infiniteQueryOptions/infiniteQueryOptions.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, expectTypeOf, test } from 'vitest' -import { type InfiniteData } from '@tanstack/query-core' -import { infiniteQueryOptions } from '../../src/infiniteQueryOptions' - -describe('queryOptions', () => { - test('Should not allow excess properties', () => { - infiniteQueryOptions({ - queryKey: ['key'], - queryFn: () => Promise.resolve('data'), - getNextPageParam: () => 1, - initialPageParam: 1, - // @ts-expect-error this is a good error, because stallTime does not exist! - stallTime: 1000, - }) - }) - test('Should infer types for callbacks', () => { - infiniteQueryOptions({ - queryKey: ['key'], - queryFn: () => Promise.resolve('data'), - staleTime: 1000, - getNextPageParam: () => 1, - initialPageParam: 1, - select: (data) => { - expectTypeOf(data).toEqualTypeOf>() - }, - }) - }) -}) diff --git a/packages/svelte-query/tests/queryOptions/queryOptions.test.ts b/packages/svelte-query/tests/queryOptions/queryOptions.test-d.ts similarity index 81% rename from packages/svelte-query/tests/queryOptions/queryOptions.test.ts rename to packages/svelte-query/tests/queryOptions/queryOptions.test-d.ts index 02ca7b1869..4999037005 100644 --- a/packages/svelte-query/tests/queryOptions/queryOptions.test.ts +++ b/packages/svelte-query/tests/queryOptions/queryOptions.test-d.ts @@ -1,6 +1,13 @@ -import { QueryClient, dataTagSymbol, skipToken } from '@tanstack/query-core' import { describe, expectTypeOf, test } from 'vitest' -import { queryOptions } from '../../src/queryOptions' +import { get } from 'svelte/store' +import { + QueriesObserver, + QueryClient, + dataTagSymbol, + skipToken, +} from '@tanstack/query-core' +import { createQueries, queryOptions } from '../../src/index' +import type { QueryObserverResult } from '@tanstack/query-core' describe('queryOptions', () => { test('Should not allow excess properties', () => { @@ -33,6 +40,19 @@ describe('queryOptions', () => { expectTypeOf(data).toEqualTypeOf() }) + test('Should work when passed to createQueries', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + + const queries = createQueries({ + queries: [options], + }) + + expectTypeOf(get(queries)[0].data).toEqualTypeOf() + }) + test('Should tag the queryKey with the result type of the QueryFn', () => { const { queryKey } = queryOptions({ queryKey: ['key'], @@ -143,4 +163,17 @@ describe('queryOptions', () => { const data = queryClient.getQueryData(options.queryKey) expectTypeOf(data).toEqualTypeOf() }) + + test('Should return the proper type when passed to QueriesObserver', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => Promise.resolve(5), + }) + + const queryClient = new QueryClient() + const queriesObserver = new QueriesObserver(queryClient, [options]) + expectTypeOf(queriesObserver).toEqualTypeOf< + QueriesObserver> + >() + }) }) From f1fb2344e2cd818f20391373b334c499f450fa7c Mon Sep 17 00:00:00 2001 From: Lachlan Collins <1667261+lachlancollins@users.noreply.github.com> Date: Sat, 20 Jul 2024 20:34:30 +1000 Subject: [PATCH 2/2] Fix createQuery types --- packages/svelte-query/src/createQuery.ts | 15 +++-- .../createQueries/createQueries.test-d.ts | 44 +++++++++++++ .../tests/createQuery/createQuery.test-d.ts | 65 +++++++++++++++++++ 3 files changed, 119 insertions(+), 5 deletions(-) create mode 100644 packages/svelte-query/tests/createQuery/createQuery.test-d.ts diff --git a/packages/svelte-query/src/createQuery.ts b/packages/svelte-query/src/createQuery.ts index 08417d54c2..32bf45e394 100644 --- a/packages/svelte-query/src/createQuery.ts +++ b/packages/svelte-query/src/createQuery.ts @@ -19,10 +19,10 @@ export function createQuery< TQueryKey extends QueryKey = QueryKey, >( options: StoreOrVal< - UndefinedInitialDataOptions + DefinedInitialDataOptions >, queryClient?: QueryClient, -): CreateQueryResult +): DefinedCreateQueryResult export function createQuery< TQueryFnData = unknown, @@ -31,13 +31,13 @@ export function createQuery< TQueryKey extends QueryKey = QueryKey, >( options: StoreOrVal< - DefinedInitialDataOptions + UndefinedInitialDataOptions >, queryClient?: QueryClient, -): DefinedCreateQueryResult +): CreateQueryResult export function createQuery< - TQueryFnData, + TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, @@ -46,6 +46,11 @@ export function createQuery< CreateQueryOptions >, queryClient?: QueryClient, +): CreateQueryResult + +export function createQuery( + options: StoreOrVal, + queryClient?: QueryClient, ) { return createBaseQuery(options, QueryObserver, queryClient) } diff --git a/packages/svelte-query/tests/createQueries/createQueries.test-d.ts b/packages/svelte-query/tests/createQueries/createQueries.test-d.ts index 32721b2f49..0f3881330a 100644 --- a/packages/svelte-query/tests/createQueries/createQueries.test-d.ts +++ b/packages/svelte-query/tests/createQueries/createQueries.test-d.ts @@ -1,6 +1,9 @@ import { describe, expectTypeOf, test } from 'vitest' import { get } from 'svelte/store' +import { skipToken } from '@tanstack/query-core' import { createQueries, queryOptions } from '../../src/index' +import type { OmitKeyof, QueryObserverResult } from '@tanstack/query-core' +import type { CreateQueryOptions } from '../../src/index' describe('createQueries', () => { test('TData should be defined when passed through queryOptions', () => { @@ -21,4 +24,45 @@ describe('createQueries', () => { expectTypeOf(data).toEqualTypeOf<{ wow: boolean }>() }) + + test('Allow custom hooks using UseQueryOptions', () => { + type Data = string + + const useCustomQueries = ( + options?: OmitKeyof, 'queryKey' | 'queryFn'>, + ) => { + return createQueries({ + queries: [ + { + ...options, + queryKey: ['todos-key'], + queryFn: () => Promise.resolve('data'), + }, + ], + }) + } + + const query = useCustomQueries() + const data = get(query)[0].data + + expectTypeOf(data).toEqualTypeOf() + }) + + test('TData should have correct type when conditional skipToken is passed', () => { + const queryResults = createQueries({ + queries: [ + { + queryKey: ['withSkipToken'], + queryFn: Math.random() > 0.5 ? skipToken : () => Promise.resolve(5), + }, + ], + }) + + const firstResult = get(queryResults)[0] + + expectTypeOf(firstResult).toEqualTypeOf< + QueryObserverResult + >() + expectTypeOf(firstResult.data).toEqualTypeOf() + }) }) diff --git a/packages/svelte-query/tests/createQuery/createQuery.test-d.ts b/packages/svelte-query/tests/createQuery/createQuery.test-d.ts new file mode 100644 index 0000000000..f2931340e9 --- /dev/null +++ b/packages/svelte-query/tests/createQuery/createQuery.test-d.ts @@ -0,0 +1,65 @@ +import { describe, expectTypeOf, test } from 'vitest' +import { get } from 'svelte/store' +import { createQuery, queryOptions } from '../../src/index' +import type { OmitKeyof } from '@tanstack/query-core' +import type { CreateQueryOptions } from '../../src/index' + +describe('createQuery', () => { + test('TData should always be defined when initialData is provided as an object', () => { + const query = createQuery({ + queryKey: ['key'], + queryFn: () => ({ wow: true }), + initialData: { wow: true }, + }) + + expectTypeOf(get(query).data).toEqualTypeOf<{ wow: boolean }>() + }) + + test('TData should be defined when passed through queryOptions', () => { + const options = queryOptions({ + queryKey: ['key'], + queryFn: () => { + return { + wow: true, + } + }, + initialData: { + wow: true, + }, + }) + const query = createQuery(options) + + expectTypeOf(get(query).data).toEqualTypeOf<{ wow: boolean }>() + }) + + test('TData should have undefined in the union when initialData is NOT provided', () => { + const query = createQuery({ + queryKey: ['key'], + queryFn: () => { + return { + wow: true, + } + }, + }) + + expectTypeOf(get(query).data).toEqualTypeOf<{ wow: boolean } | undefined>() + }) + + test('Allow custom hooks using CreateQueryOptions', () => { + type Data = string + + const useCustomQuery = ( + options?: OmitKeyof, 'queryKey' | 'queryFn'>, + ) => { + return createQuery({ + ...options, + queryKey: ['todos-key'], + queryFn: () => Promise.resolve('data'), + }) + } + + const query = useCustomQuery() + + expectTypeOf(get(query).data).toEqualTypeOf() + }) +})