query icon indicating copy to clipboard operation
query copied to clipboard

TypeScript: Selectors for inifinite query less expressive

Open jasperkuperus opened this issue 3 years ago • 12 comments

Describe the bug Selectors in combination with useQuery look very good. Creating multiple hooks on top of the original useQuery hook, allowing you to get different parts of the data out the query result as described in https://tkdodo.eu/blog/react-query-data-transformations in particular (e.g. useTodosCount, useTodos, useTodo(id)).

Now, when using useInfiniteQuery, I'm able to do the same thing. But TypeScript won't let me. It forces me into the InfiniteData type, hence the { pages, pageParams } format.

To Reproduce

What I'd like to do is twofold:

  • Get an array of all items concatenated over all pages
  • Get the total count of items available in the backend

Type definitions of a simplified example:

interface Entity {
  id: string;
  // ...
}

interface APIResponse {
  total: number;      // Total number of entities available in the backend
  totalPages: number; // Total number of pages with entities
  items: Entity[];    // The entities for this requested page
}

interface DesiredDataResult {
  total: number;   // Same value as total in APIResponse
  items: Entity[]; // Concatenation of all items on all fetched pages (flatMap of `page.items`)
}

Now, I try to build the DesiredDataResult in the selector:

const { data: result } = useInfiniteQuery(
  ['entities', 'list'],
  async ({ pageParam = 0 }) => getPageFromAPI(pageParam),
  {
    getNextPageParam: (lastPage, allPages) =>
      lastPage.totalPages > allPages.length ? allPages.length : undefined,
    select: React.useCallback(
      (data: InfiniteData<APIResponse>) => ({
        total: data.pages[0].total,
        entities: data.pages.flatMap((page) => page.items),
      }),
      [],
    ),
  },
);

And I get this TypeScript error. It wants me to use the { pages, pageParams } type.

Type '(data: InfiniteData<APIResponse>) => { total: number; entities: Entity[]; }' is not assignable to type '(data: InfiniteData<APIResponse>) => InfiniteData<APIResponse>'.
  Type '{ total: number; entities: Entity[]; }' is missing the following properties from type 'InfiniteData<APIResponse>': pages, pageParams ts(2322)

Therefore, selectors in combination with useInfiniteQuery are less expressive than with useQuery. Maybe I'm missing something?

Expected behavior I would have expected to be able to apply selectors with the same expressiveness as useQuery on useInfiniteQuery.

Screenshots Screenshot 2021-12-07 at 09 03 29

Desktop (please complete the following information):

  • OS: macOS
  • Browser: Safari
  • Version: 14.1.2

jasperkuperus avatar Dec 07 '21 08:12 jasperkuperus

@jasperkuperus I think I was able to do this for v4. can you have a test with the codesandbox preview build to see if that screwed something up?

https://github.com/tannerlinsley/react-query/pull/3088#issuecomment-991760032

TkDodo avatar Dec 11 '21 19:12 TkDodo

@TkDodo I've forked the TypeScript sandbox from your PR and converted the useQuery into useInfiniteQuery, faking an infinite scroll by keeping the posts HTTP request with just 1 page. In this sandbox, I've chosen to just return an array with all Posts from select. I've tried with and without the generics.

Check out line 56, it has a console.log with the data, which you can use to easily inspect the type of the data, it's still InfiniteData<...>.

Screenshot 2021-12-12 at 17 12 51

What you can see in this sandbox, is that it works like a charm. The only culprit are the typings 😊

jasperkuperus avatar Dec 12 '21 16:12 jasperkuperus

Do you have a link to that sandbox?

TkDodo avatar Dec 12 '21 16:12 TkDodo

Oops, sorry about that: https://codesandbox.io/s/tannerlinsley-react-query-basic-typescript-forked-qh3e6?file=/src/index.tsx:166-169 🥲

jasperkuperus avatar Dec 12 '21 17:12 jasperkuperus

@jasperkuperus thanks, you are right. I also need to adapt the UseInfiniteQueryResult- not only the InfiniteQueryObserverOptions. Also, I better add some type assertion tests because I totally missed this :)

TkDodo avatar Dec 12 '21 19:12 TkDodo

I sadly couldn't get anywhere. This is super complicated. I got stuck with fetchNextPage / fetchPreviousPage. Not even sure if the return value of these functions runs through select or not.

Since this is the 3rd time that I've tried to implement this, thinking "how hard can it be", I'm gonna leave this note to myself: It's quite hard.

Feel free to pick it up if you want :)

TkDodo avatar Dec 17 '21 09:12 TkDodo

sorry, I wanted to close the PR, not the issue

TkDodo avatar Dec 17 '21 09:12 TkDodo

I could give it a shot, but I have no idea when I'll be able to find the time for it. Maybe the great @tannerlinsley himself? 😃

jasperkuperus avatar Dec 17 '21 09:12 jasperkuperus

yes please @jasperkuperus, feel free to go for it 🚀

TkDodo avatar Dec 27 '21 21:12 TkDodo

I was recently able to solve this by editing a few types. I am not too familiar with code base but it essentially works like this, can make PR if it would be workable for y'all.

Update the Type for InfiniteData to except a second type parameter

export type InfiniteData<TData, EX extends Record<unknown, unknown> = unknown> = {
    pages: TData[];
    pageParams: unknown[];
} & EX

Now we will return any additions we make to the select as the following:

InfiniteData<APITradingRewardHistoryPaginated, { items: ItemsWithStatus }>
image

We can now see that useInfiniteQuery is picking up the SELECT from my select call (new type param added)

image

as well as properly typing the data.items:

image

Update the InfiniteQuery types to pass SELECT extends Record<unknown, unknown> = unknown

All we need to do is add a SELECT (or something to match your conventions, i just did it to see if i could) all the way through so it can infer it properly.

export declare function useInfiniteQuery<TQueryFnData = unknown, TError = unknown, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, SELECT extends Record<unknown, unknown> =  Record<unknown, unknown>>(options: UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryFnData, TQueryKey, SELECT>): UseInfiniteQueryResult<TData, TError, SELECT>;

It all boils down to those selects passing through to the type:

export interface InfiniteQueryObserverSuccessResult<TData = unknown, TError = unknown,  SELECT = unknown> extends InfiniteQueryObserverBaseResult<TData, TError, SELECT> {
    data: InfiniteData<TData, SELECT>;

The rule here would be that you need to return the standard value, but can add additional keys and they will be picked up and typed properly.

bradennapier avatar May 19 '22 03:05 bradennapier

Actually, interestingly, it works without defining the type to return which is cool!

image

It actually overwrites pages as well so it works as expected:

image

bradennapier avatar May 19 '22 04:05 bradennapier

Went ahead and got it working in the actual repo and submitted a PR for your review 😇

bradennapier avatar May 19 '22 06:05 bradennapier

we have this working in v5 via:

  • #5004

TkDodo avatar Feb 26 '23 18:02 TkDodo

Thanks, that's great, thanks for the follow-up!

jasperkuperus avatar Feb 27 '23 06:02 jasperkuperus