query icon indicating copy to clipboard operation
query copied to clipboard

`TData` generic can be inferred or specified incorrectly, causing unexpected runtime errors.

Open braeden opened this issue 11 months ago • 3 comments

Describe the bug

Related to https://github.com/TanStack/query/issues/4770, revitalizing since NoInfer/overloads can at least partially help.

  • TQueryFnData is directly inferred from queryFn specified in the hook options options
  • TData defaults to TQueryFnData, but can be specified via a generic or annotated return type
  • When TData generic is populated (by inference, or explicitly) , nothing enforces that there's type compatibility until you put a select function, leading to uncaught runtime breaks.

Your minimal, reproducible example

https://codesandbox.io/p/devbox/vigilant-forest-7j45zd?file=%2Fsrc%2Findex.tsx%3A58%2C1

Steps to reproduce

See that usePosts and usePosts2 both have a data type claiming to be number, but are instead a Post at runtime.

Examples:

function usePosts() {
  return useQuery<Post[], Error, number, string[]>({
    queryKey: ['posts'],
    queryFn: async (): Promise<Array<Post>> => {
      const response = await fetch(
        'https://jsonplaceholder.typicode.com/posts'
      );
      return await response.json();
    },
  });
}

function usePosts2(): UseQueryResult<number, Error> {
  return useQuery({
    queryKey: ['posts'],
    queryFn: async (): Promise<Array<Post>> => {
      const response = await fetch(
        'https://jsonplaceholder.typicode.com/posts'
      );
      return await response.json();
    },
  });
}

Expected behavior

I expect to get a type-error, because I haven't specified a select function and TQueryFnData does not equal TData, therefore we have easy, uncaught runtime breaks.

Potential solutions:

  • Using NoInfer on the UseQueryResult<NoInfer<TData>, TError>, will at least prevent usePost2 issue
  • We should be able to write a conditional type by using additional overloads (untested/hypothetically) to narrow on the condition where we have a select specified.
type UseQueryOptionsWithSelect = ...
type UseQueryOptionsWithoutSelect = ...

declare function useQuery<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(options: UseQueryOptionsWithoutSelect<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): UseQueryResult<TQueryFnData extends TData ? NoInfer<TData> : never, TError>;
declare function useQuery<TQueryFnData = unknown, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey>(options: UseQueryOptionsWithSelect<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): UseQueryResult<NoInfer<TData>, TError>;

How often does this bug happen?

None

Screenshots or Videos

No response

Platform

N/A

Tanstack Query adapter

None

TanStack Query version

5.66.0

TypeScript version

5.7.2

Additional context

No response

braeden avatar Feb 12 '25 18:02 braeden

Using NoInfer on the UseQueryResult<NoInfer<TData>, TError>, will at least prevent usePost2 issue

If that fixes it, please open a PR with that fix, including the appropriate type tests.

We should be able to write a conditional type by using additional overloads

We don’t want more overloads. If you add angle brackets <> to useQuery, you are basically doing a type assertion. I don’t want to add more complexity for that, as it’s not a recommended thing to do, as documented here: https://tanstack.com/query/v5/docs/framework/react/typescript

TkDodo avatar Feb 14 '25 10:02 TkDodo

Started draft PR here https://github.com/TanStack/query/pull/8654 -- just indexed on useQuery to get first thoughts/before digging into how to more generally apply NoInfer to query-core types.

braeden avatar Feb 16 '25 00:02 braeden

I understood the point to be that mismatches between TData and TQueryFnData don't raise a type error.

However, when I actually run the above code in the code sandbox, I get a type mismatch error in the return part, and when I run the build, I get an error and it doesn't build. Please let me know if there's something I'm missing.

Image

novice0840 avatar May 26 '25 11:05 novice0840