-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(types): useQueries to flow through types #1527
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This pull request is being automatically deployed with Vercel (learn more). 🔍 Inspect: https://vercel.com/tannerlinsley/react-query/m1scq9ht1 |
@@ -5,15 +5,26 @@ import type { QueryClient } from './queryClient' | |||
import { QueryObserver } from './queryObserver' | |||
import { Subscribable } from './subscribable' | |||
|
|||
type QueriesObserverListener = (result: QueryObserverResult[]) => void |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This had to move inline as it relies upon the generic types inside the QueriesObserver
Ah - I can see I've upset the rules of hooks in the test. I'll fix that but have to break off for now. |
Fixed. |
Hi @johnnyreilly, thanks for the PR! It seems like all queries need to return the same data? Think most of the time each query would require a different type. And is it possible to infer the return types from the given options? |
The examples of
The way it works is through type inference, and so yes, if there are various types returned, that should flow through too! |
Great, could you add some tests for those cases? Same type: const result = useQueries([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 2 },
])
expectType<number | undefined>(result[0].data)
expectType<number | undefined>(result[1].data) Different type: const result = useQueries([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 'data' },
])
expectType<number | undefined>(result[0].data)
expectType<string | undefined>(result[1].data) |
Yup, the same type case is covered by the test I added. It's a good shout to cover the different type case as well. I'll try to add when I'm next near a keyboard |
I've added a test that covers the different type use case as well. It's actually subtly different than I anticipated, where there is a variety of return types you need to specify the variety of types explicitly (or do It's arguable that this would make it a breaking change from a type perspective (though for the atypical use case). |
Ideally users should not have to specify generics explicitly and it would be great if the types could be inferred correctly for each query instead of creating a union. Would you be able to get that working? |
I agree that would be ideal - I've had a go but I've been unsuccessful. It may not be possible with the TypeScript type system as is - or beyond my abilities. The plus with this approach is that specifying the generics is only necessary for the edge case. Not perfect but an improvement on the current casting always scenario |
No problem! Although not perfect, it does add typing for some use cases. Does it default to const result = useQueries([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 'two' },
]) |
To take your specific case: const resultWithoutUsingMap = useQueries([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 'two' },
]) The compiler will complain that the second array element is not a match for the first; so no defaulting to Type '() => string' is not assignable to type 'QueryFunction<number>'.
Type 'string' is not assignable to type 'number | Promise<number>'. This is remedied by supplying the types like so: const resultWithoutUsingMap = useQueries<number | string>([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 'two' },
]) However, there's a better choice; using const resultWithAllTheSameTypes = useQueries(
[1, 2].map(x => ({ queryKey: `${x}`, queryFn: () => x }))
)
// resultWithAllTheSameTypes: QueryObserverResult<number, unknown>[]
const resultWithDifferentTypes = useQueries(
[1, 'two', new Date()].map(x => ({ queryKey: `${x}`, queryFn: () => x }))
)
// resultWithDifferentTypes: QueryObserverResult<string | number | Date, unknown>[] Type inference in all cases that use |
Hmm, I guess we will have to supply the types with generics then a lot, because I don't think that e.g.
will be helpful a lot. The first element in the Array will be What's also sub-optimal about the |
True - but this is already the case with
Agree it would be, it doesn't look like the compiler supports this use case as yet. Certainly as far as I'm aware.
It probably depends how you're using it. I've a project that heavily uses Small side note: it was that project that lead me to contributing this PR. My motivation was disatisfaction with the current choice between lack of type safety with recasting after each |
@johnnyreilly I agree that this PR makes the current situation a lot better, because queries are now typable at all as compared to not-at-all before, so thanks for your contribution 👍 . I just wanted to see if we can take it any further, but it doesn't seem easily doable. The |
I do like the additional typing, but I do not like that it will error in a valid use case: const result = useQueries([
{ queryKey: key1, queryFn: () => 1 },
{ queryKey: key2, queryFn: () => 'two' },
])
// compiler error This will probably surprise users and they might not know how to fix it... |
That's a reasonable concern. I'd be happy to add documentation which covers this. |
Proposed docs to add to the TypeScript usersIf you're using
In both the examples above, no types were specified and the compiler correctly inferred the types from the array passed to However, if you pass an array literal where different elements have a
This will result in an error along the lines of This can be remedied by supplying the union of possible data types as a type parameter; like so:
|
I've added the docs - happy to tweak. |
I don't think the I'm not entirely sure how to proceed here. The PR adds typing for single query functions, but at the same time it introduces a compiler error when using multiple query functions. And even when users fix the compiler error by explicitly specifying the return type union of all query functions, it wouldn't be that useful because users would still need to narrow down the results. Personally I would prefer an implementation which covers all cases, or if not possible, one which degrades gracefully. This is a matter of opinion though, maybe others think differently. Would just adding the ability to specify a return type without type inference be an option? That would at least be a bit more robust. |
No - it's variadic tuple types that allows the strong types to flow through in the positional way desired (see discussion above). So it would be a breaking change if taken. |
On the other points:
I think this tweak resolves that: import { useQueries, UseQueryOptions, UseQueryResult } from 'react-query';
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
export function useQueriesTyped<
TQueries extends readonly UseQueryOptions<TData, TError>[],
TQueryFnData = unknown,
TError = unknown,
TData = TQueryFnData
>(
...queries: [...TQueries]
): {
[ArrayElement in keyof TQueries]: UseQueryResult<
Awaited<ReturnType<NonNullable<Extract<TQueries[ArrayElement], UseQueryOptions>['queryFn']>>>,
TError
>;
} {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return useQueries(queries as UseQueryOptions<unknown, unknown, unknown>[]) as any;
}
I don't think this is possible - I have experimented without success.
Not sure what this refers to. |
If I strip the spread operator from
Can you give an example on how the usage would look like?
This also applies to other properties like
It refers to the |
Yeah - you're right; I've applied the change to the PR. My proposed solution around errors didn't work out so I've undone that. With regards the So the PR is now non-breaking changes; just a feature. |
The one I am most concerned about is the const results = useQueries([{ queryKey: 'key', queryFn: () => ({ prop: 'value' }), select: x => x.prop }])
results[0].data.prop // <-- TS thinks this is fine while data is actually a string While I would love to see additional type inference for |
As I understand it there's no change in behaviour here. This PR doesn't solve the problem of type inference for I've tested with the present Both versions error out with this: Object is of type 'unknown'.ts(2571) on select: x => x.prop Where we try to So this PR doesn't change the experience of using |
Oh wait - I see what
You're right - this is a problem |
Okay I've been digging away on this and it looks like flowing through the So I've taken a different tack. If a But if |
Seems like you would be able to use the return type of the select function conditionally, but I'm not sure... I need to learn more TS 😂 |
Let me experiment.... |
It works! There's one caveat but it's minor:
The thing that isn't possible, is flowing through the data type to the input of the However, as suggested by @tannerlinsley, we can get the return type of The PR has been updated to use this combined approach (and test has been added to cover it). So this PR now represents the addition of strong typing to |
Couple of thoughts: Current implementation
This implementation
This is probably as far as it can get without existential generic types. Both implementations have pros and cons, so I guess it comes down to what "we" think is more important. |
Thanks for commenting @boschni. I agree the current implementation isn't perfect with regards to having to supply type assertions with For my money the advantage of taking this is reduced bugs. Using the current implementation it's unfortunately easy to inadvertently create a bug by using type assertions incorrectly. I know this as I did. 😄 Others are bumping on this too - from the sounds of it @coffenbacher is in that category - as is @matthewdavidrodgers in #1675. My view is that it would be a good idea to merge this as is. The typed approach has been working well for my own use cases and I'm not aware of any problematic use cases. But I appreciate I'm biased 😉 I wanted to suggest another approach that we could take. Rather than having this become the canonical The advantage of this approach is that it's "in-the-box" but opt-in or opt-outable (depending upon whether it's the non default or default implementation respectively). Essentially it allows people to opt in and out as they so choose. As I say, my view is that this is good to land as is. But pivoting to the dual approach would still be a good outcome. |
Marking as stale and closing for now, but would like to see some more movement here in the future. Comment or reopen if/when more information comes. |
If there's something I can do to advance this I'd love to. I've suggested a few proposed ways forward here: #1527 (comment) If one of these works, or there's an alternative you have in mind then please do suggest it. |
Hello @tannerlinsley!
Merry Christmas and thank you for
react-query
; it's awesome! 🎄I've just been trying out
useQueries
inreact-query
3 and it's really cool! However one of the things I bumped on was the return type reliably beingQueryObserverResult<unknown, unknown>[]
. This means I have to assert the type before I can use it.This can be improved by supporting generics for
useQueries
just asuseQuery
does. This PR adds that support; very much modelled on the code ofuseQuery
. It also adds a test which asserts that types flow through in the way we'd hope for.This should be a backwards compatible change; all existing users will be experiencing
QueryObserverResult<unknown, unknown>[]
and then having to subsequently assert types. That should all still work, but if they would like, they'll no longer need that code.What do you think?