diff --git a/.changeset/noinfer-infinite-suspense-hooks.md b/.changeset/noinfer-infinite-suspense-hooks.md new file mode 100644 index 00000000000..c71d490a517 --- /dev/null +++ b/.changeset/noinfer-infinite-suspense-hooks.md @@ -0,0 +1,6 @@ +--- +'@tanstack/react-query': patch +'@tanstack/preact-query': patch +--- + +Add `NoInfer` to the return types of `useInfiniteQuery`, `useSuspenseQuery`, and `useSuspenseInfiniteQuery` so that an explicitly annotated result type can no longer reverse-infer `TData`, matching `useQuery`. A distributive wrapper is used so discriminated-union narrowing on `data` keeps working. diff --git a/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx b/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx index b11002c9e2c..b6f39b12ca1 100644 --- a/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx +++ b/packages/preact-query/src/__tests__/useInfiniteQuery.test-d.tsx @@ -3,6 +3,7 @@ import type { InfiniteData } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { describe, expectTypeOf, it } from 'vitest' +import type { UseInfiniteQueryResult } from '../types' import { useInfiniteQuery } from '../useInfiniteQuery' describe('pageParam', () => { @@ -142,3 +143,39 @@ describe('error booleans', () => { expectTypeOf(isRefetchError).toEqualTypeOf() }) }) + +describe('NoInfer', () => { + // eslint-disable-next-line vitest/expect-expect + it('TData should depend only on the arguments, not the annotated result', () => { + // @ts-expect-error + const result: UseInfiniteQueryResult> = + useInfiniteQuery({ + queryKey: queryKey(), + queryFn: () => ({ wow: true }), + initialPageParam: 1, + getNextPageParam: () => undefined, + }) + + void result + }) + + it('should preserve discriminated-union narrowing on data', () => { + type Item = + | { type: 'first'; first: string } + | { type: 'second'; second: string } + + const { data } = useInfiniteQuery({ + queryKey: queryKey(), + queryFn: (): Item => ({ type: 'first', first: 'a' }), + initialPageParam: 1, + getNextPageParam: () => undefined, + select: (infiniteData) => infiniteData.pages[0], + }) + + const second = data?.type === 'first' ? undefined : data + + expectTypeOf(second).toEqualTypeOf< + { type: 'second'; second: string } | undefined + >() + }) +}) diff --git a/packages/preact-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx b/packages/preact-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx index d2b3835de08..db074769d1e 100644 --- a/packages/preact-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx +++ b/packages/preact-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx @@ -3,6 +3,7 @@ import type { InfiniteData } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { assertType, describe, expectTypeOf, it } from 'vitest' +import type { UseSuspenseInfiniteQueryResult } from '../types' import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery' describe('useSuspenseInfiniteQuery', () => { @@ -93,3 +94,39 @@ describe('useSuspenseInfiniteQuery', () => { expectTypeOf(query).not.toHaveProperty('isPlaceholderData') }) }) + +describe('NoInfer', () => { + // eslint-disable-next-line vitest/expect-expect + it('TData should depend only on the arguments, not the annotated result', () => { + // @ts-expect-error + const result: UseSuspenseInfiniteQueryResult> = + useSuspenseInfiniteQuery({ + queryKey: queryKey(), + queryFn: () => ({ wow: true }), + initialPageParam: 1, + getNextPageParam: () => 1, + }) + + void result + }) + + it('should preserve discriminated-union narrowing on data', () => { + type Item = + | { type: 'first'; first: string } + | { type: 'second'; second: string } + + const { data } = useSuspenseInfiniteQuery({ + queryKey: queryKey(), + queryFn: (): Item => ({ type: 'first', first: 'a' }), + initialPageParam: 1, + getNextPageParam: () => 1, + select: (infiniteData) => infiniteData.pages[0], + }) + + const second = data?.type === 'first' ? undefined : data + + expectTypeOf(second).toEqualTypeOf< + { type: 'second'; second: string } | undefined + >() + }) +}) diff --git a/packages/preact-query/src/__tests__/useSuspenseQuery.test-d.tsx b/packages/preact-query/src/__tests__/useSuspenseQuery.test-d.tsx index 53adae90660..d5ff57bfbaf 100644 --- a/packages/preact-query/src/__tests__/useSuspenseQuery.test-d.tsx +++ b/packages/preact-query/src/__tests__/useSuspenseQuery.test-d.tsx @@ -2,6 +2,7 @@ import { skipToken } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { assertType, describe, expectTypeOf, it } from 'vitest' +import type { UseSuspenseQueryResult } from '../types' import { useSuspenseQuery } from '../useSuspenseQuery' describe('useSuspenseQuery', () => { @@ -88,3 +89,33 @@ describe('useSuspenseQuery', () => { } }) }) + +describe('NoInfer', () => { + // eslint-disable-next-line vitest/expect-expect + it('TData should depend only on the arguments, not the annotated result', () => { + // @ts-expect-error + const result: UseSuspenseQueryResult<{ wow: string }> = useSuspenseQuery({ + queryKey: queryKey(), + queryFn: () => ({ wow: true }), + }) + + void result + }) + + it('should preserve discriminated-union narrowing on data', () => { + type Result = + | { type: 'first'; first: string } + | { type: 'second'; second: string } + + const { data } = useSuspenseQuery({ + queryKey: queryKey(), + queryFn: (): Result => ({ type: 'first', first: 'a' }), + }) + + const second = data.type === 'first' ? undefined : data + + expectTypeOf(second).toEqualTypeOf< + { type: 'second'; second: string } | undefined + >() + }) +}) diff --git a/packages/preact-query/src/useInfiniteQuery.ts b/packages/preact-query/src/useInfiniteQuery.ts index f78c14a2375..e4f8617b180 100644 --- a/packages/preact-query/src/useInfiniteQuery.ts +++ b/packages/preact-query/src/useInfiniteQuery.ts @@ -18,6 +18,8 @@ import type { } from './types' import { useBaseQuery } from './useBaseQuery' +type NarrowableNoInfer = T extends unknown ? NoInfer : never + export function useInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -33,7 +35,7 @@ export function useInfiniteQuery< TPageParam >, queryClient?: QueryClient, -): DefinedUseInfiniteQueryResult +): DefinedUseInfiniteQueryResult, TError> export function useInfiniteQuery< TQueryFnData, @@ -50,7 +52,7 @@ export function useInfiniteQuery< TPageParam >, queryClient?: QueryClient, -): UseInfiniteQueryResult +): UseInfiniteQueryResult, TError> export function useInfiniteQuery< TQueryFnData, @@ -67,7 +69,7 @@ export function useInfiniteQuery< TPageParam >, queryClient?: QueryClient, -): UseInfiniteQueryResult +): UseInfiniteQueryResult, TError> export function useInfiniteQuery( options: UseInfiniteQueryOptions, diff --git a/packages/preact-query/src/useSuspenseInfiniteQuery.ts b/packages/preact-query/src/useSuspenseInfiniteQuery.ts index 54e52f752dc..39a4bdc2bf6 100644 --- a/packages/preact-query/src/useSuspenseInfiniteQuery.ts +++ b/packages/preact-query/src/useSuspenseInfiniteQuery.ts @@ -15,6 +15,8 @@ import type { } from './types' import { useBaseQuery } from './useBaseQuery' +type NarrowableNoInfer = T extends unknown ? NoInfer : never + export function useSuspenseInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -30,7 +32,7 @@ export function useSuspenseInfiniteQuery< TPageParam >, queryClient?: QueryClient, -): UseSuspenseInfiniteQueryResult { +): UseSuspenseInfiniteQueryResult, TError> { if (process.env.NODE_ENV !== 'production') { if ((options.queryFn as any) === skipToken) { console.error('skipToken is not allowed for useSuspenseInfiniteQuery') diff --git a/packages/preact-query/src/useSuspenseQuery.ts b/packages/preact-query/src/useSuspenseQuery.ts index ad2ff4cca63..951f13ccda6 100644 --- a/packages/preact-query/src/useSuspenseQuery.ts +++ b/packages/preact-query/src/useSuspenseQuery.ts @@ -5,6 +5,8 @@ import { defaultThrowOnError } from './suspense' import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types' import { useBaseQuery } from './useBaseQuery' +type NarrowableNoInfer = T extends unknown ? NoInfer : never + export function useSuspenseQuery< TQueryFnData = unknown, TError = DefaultError, @@ -13,7 +15,7 @@ export function useSuspenseQuery< >( options: UseSuspenseQueryOptions, queryClient?: QueryClient, -): UseSuspenseQueryResult { +): UseSuspenseQueryResult, TError> { if (process.env.NODE_ENV !== 'production') { if ((options.queryFn as any) === skipToken) { console.error('skipToken is not allowed for useSuspenseQuery') diff --git a/packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx b/packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx index b4784aa4b09..6d1dc5432ce 100644 --- a/packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx +++ b/packages/react-query/src/__tests__/useInfiniteQuery.test-d.tsx @@ -3,6 +3,7 @@ import { QueryClient } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { useInfiniteQuery } from '../useInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' +import type { UseInfiniteQueryResult } from '../types' describe('pageParam', () => { it('initialPageParam should define type of param passed to queryFunctionContext', () => { @@ -141,3 +142,39 @@ describe('error booleans', () => { expectTypeOf(isRefetchError).toEqualTypeOf() }) }) + +describe('NoInfer', () => { + // eslint-disable-next-line vitest/expect-expect + it('TData should depend only on the arguments, not the annotated result', () => { + // @ts-expect-error + const result: UseInfiniteQueryResult> = + useInfiniteQuery({ + queryKey: queryKey(), + queryFn: () => ({ wow: true }), + initialPageParam: 1, + getNextPageParam: () => undefined, + }) + + void result + }) + + it('should preserve discriminated-union narrowing on data', () => { + type Item = + | { type: 'first'; first: string } + | { type: 'second'; second: string } + + const { data } = useInfiniteQuery({ + queryKey: queryKey(), + queryFn: (): Item => ({ type: 'first', first: 'a' }), + initialPageParam: 1, + getNextPageParam: () => undefined, + select: (infiniteData) => infiniteData.pages[0], + }) + + const second = data?.type === 'first' ? undefined : data + + expectTypeOf(second).toEqualTypeOf< + { type: 'second'; second: string } | undefined + >() + }) +}) diff --git a/packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx b/packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx index e656e8cb50c..7018c9043f1 100644 --- a/packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx +++ b/packages/react-query/src/__tests__/useSuspenseInfiniteQuery.test-d.tsx @@ -3,6 +3,7 @@ import { skipToken } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { useSuspenseInfiniteQuery } from '../useSuspenseInfiniteQuery' import type { InfiniteData } from '@tanstack/query-core' +import type { UseSuspenseInfiniteQueryResult } from '../types' describe('useSuspenseInfiniteQuery', () => { it('should always have data defined', () => { @@ -92,3 +93,39 @@ describe('useSuspenseInfiniteQuery', () => { expectTypeOf(query).not.toHaveProperty('isPlaceholderData') }) }) + +describe('NoInfer', () => { + // eslint-disable-next-line vitest/expect-expect + it('TData should depend only on the arguments, not the annotated result', () => { + // @ts-expect-error + const result: UseSuspenseInfiniteQueryResult> = + useSuspenseInfiniteQuery({ + queryKey: queryKey(), + queryFn: () => ({ wow: true }), + initialPageParam: 1, + getNextPageParam: () => 1, + }) + + void result + }) + + it('should preserve discriminated-union narrowing on data', () => { + type Item = + | { type: 'first'; first: string } + | { type: 'second'; second: string } + + const { data } = useSuspenseInfiniteQuery({ + queryKey: queryKey(), + queryFn: (): Item => ({ type: 'first', first: 'a' }), + initialPageParam: 1, + getNextPageParam: () => 1, + select: (infiniteData) => infiniteData.pages[0], + }) + + const second = data?.type === 'first' ? undefined : data + + expectTypeOf(second).toEqualTypeOf< + { type: 'second'; second: string } | undefined + >() + }) +}) diff --git a/packages/react-query/src/__tests__/useSuspenseQuery.test-d.tsx b/packages/react-query/src/__tests__/useSuspenseQuery.test-d.tsx index 6e0fb5534bc..acb03d582be 100644 --- a/packages/react-query/src/__tests__/useSuspenseQuery.test-d.tsx +++ b/packages/react-query/src/__tests__/useSuspenseQuery.test-d.tsx @@ -2,6 +2,7 @@ import { assertType, describe, expectTypeOf, it } from 'vitest' import { skipToken } from '@tanstack/query-core' import { queryKey } from '@tanstack/query-test-utils' import { useSuspenseQuery } from '../useSuspenseQuery' +import type { UseSuspenseQueryResult } from '../types' describe('useSuspenseQuery', () => { it('should always have data defined', () => { @@ -87,3 +88,33 @@ describe('useSuspenseQuery', () => { } }) }) + +describe('NoInfer', () => { + // eslint-disable-next-line vitest/expect-expect + it('TData should depend only on the arguments, not the annotated result', () => { + // @ts-expect-error + const result: UseSuspenseQueryResult<{ wow: string }> = useSuspenseQuery({ + queryKey: queryKey(), + queryFn: () => ({ wow: true }), + }) + + void result + }) + + it('should preserve discriminated-union narrowing on data', () => { + type Result = + | { type: 'first'; first: string } + | { type: 'second'; second: string } + + const { data } = useSuspenseQuery({ + queryKey: queryKey(), + queryFn: (): Result => ({ type: 'first', first: 'a' }), + }) + + const second = data.type === 'first' ? undefined : data + + expectTypeOf(second).toEqualTypeOf< + { type: 'second'; second: string } | undefined + >() + }) +}) diff --git a/packages/react-query/src/useInfiniteQuery.ts b/packages/react-query/src/useInfiniteQuery.ts index 32ebfb7673e..4074880eecd 100644 --- a/packages/react-query/src/useInfiniteQuery.ts +++ b/packages/react-query/src/useInfiniteQuery.ts @@ -18,6 +18,8 @@ import type { UndefinedInitialDataInfiniteOptions, } from './infiniteQueryOptions' +type NarrowableNoInfer = T extends unknown ? NoInfer : never + export function useInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -33,7 +35,7 @@ export function useInfiniteQuery< TPageParam >, queryClient?: QueryClient, -): DefinedUseInfiniteQueryResult +): DefinedUseInfiniteQueryResult, TError> export function useInfiniteQuery< TQueryFnData, @@ -50,7 +52,7 @@ export function useInfiniteQuery< TPageParam >, queryClient?: QueryClient, -): UseInfiniteQueryResult +): UseInfiniteQueryResult, TError> export function useInfiniteQuery< TQueryFnData, @@ -67,7 +69,7 @@ export function useInfiniteQuery< TPageParam >, queryClient?: QueryClient, -): UseInfiniteQueryResult +): UseInfiniteQueryResult, TError> export function useInfiniteQuery( options: UseInfiniteQueryOptions, diff --git a/packages/react-query/src/useSuspenseInfiniteQuery.ts b/packages/react-query/src/useSuspenseInfiniteQuery.ts index 3c2fdfdd72f..38ab779c4b1 100644 --- a/packages/react-query/src/useSuspenseInfiniteQuery.ts +++ b/packages/react-query/src/useSuspenseInfiniteQuery.ts @@ -15,6 +15,8 @@ import type { UseSuspenseInfiniteQueryResult, } from './types' +type NarrowableNoInfer = T extends unknown ? NoInfer : never + export function useSuspenseInfiniteQuery< TQueryFnData, TError = DefaultError, @@ -30,7 +32,7 @@ export function useSuspenseInfiniteQuery< TPageParam >, queryClient?: QueryClient, -): UseSuspenseInfiniteQueryResult { +): UseSuspenseInfiniteQueryResult, TError> { if (process.env.NODE_ENV !== 'production') { if ((options.queryFn as any) === skipToken) { console.error('skipToken is not allowed for useSuspenseInfiniteQuery') diff --git a/packages/react-query/src/useSuspenseQuery.ts b/packages/react-query/src/useSuspenseQuery.ts index 7dfdb064778..c8aeab172d6 100644 --- a/packages/react-query/src/useSuspenseQuery.ts +++ b/packages/react-query/src/useSuspenseQuery.ts @@ -5,6 +5,8 @@ import { defaultThrowOnError } from './suspense' import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types' import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' +type NarrowableNoInfer = T extends unknown ? NoInfer : never + export function useSuspenseQuery< TQueryFnData = unknown, TError = DefaultError, @@ -13,7 +15,7 @@ export function useSuspenseQuery< >( options: UseSuspenseQueryOptions, queryClient?: QueryClient, -): UseSuspenseQueryResult { +): UseSuspenseQueryResult, TError> { if (process.env.NODE_ENV !== 'production') { if ((options.queryFn as any) === skipToken) { console.error('skipToken is not allowed for useSuspenseQuery')