query
query copied to clipboard
TypeScript: Selectors for inifinite query less expressive
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
Desktop (please complete the following information):
- OS: macOS
- Browser: Safari
- Version: 14.1.2
@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 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 Post
s 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<...>
.

What you can see in this sandbox, is that it works like a charm. The only culprit are the typings 😊
Do you have a link to that sandbox?
Oops, sorry about that: https://codesandbox.io/s/tannerlinsley-react-query-basic-typescript-forked-qh3e6?file=/src/index.tsx:166-169 🥲
@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 :)
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 :)
sorry, I wanted to close the PR, not the issue
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? 😃
yes please @jasperkuperus, feel free to go for it 🚀
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 }>

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

as well as properly typing the data.items:

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.
Actually, interestingly, it works without defining the type to return which is cool!

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

Went ahead and got it working in the actual repo and submitted a PR for your review 😇
we have this working in v5 via:
- #5004
Thanks, that's great, thanks for the follow-up!