Incomprehensible type mismatch with query generics
StrikeAgainst opened this issue · 3 comments
Issue Description
In an effort to abstract the use of queries for various similar situations, I wrapped a query inside a function typed with an extended generic for the result TData
. When applying this composable onto a variable typed with a more general result type, TS is giving me an error that does not make much sense to me. Below I have a condensed use case for the issue:
const getById = <TData extends Record<string, string>>(gql: DocumentNode, id: string) => {
return useQuery<TData>(
gql,
computed(() => ({ id }))
)
}
type ByIdQuery<TData extends Record<string, string>> = ReturnType<typeof getById<TData>>
const query: ByIdQuery<Record<string, string>> = getById<{ foo: string }>(MyQuery, "1") <--- error
const queryFoo: ByIdQuery<{ foo: string }> = getById<{ foo: string }>(MyQuery, "1")
Basically TData
extends Record<string, string>
, where { foo: string }
in theory should be assignable onto. While I can assign the return value of getById<{ foo: string }>
to a variable of ByIdQuery<{ foo: string }>
, I cannot apply it onto the more general type of ByIdQuery<Record<string, string>>
. In the error stack, somewhere along the line, Record<string, string>
and { foo: string; }
are suddenly swapped then, with no indication as to why this happens.
Type 'NextFetchPolicyContext<{ foo: string; }, OperationVariables>' is not assignable to type 'NextFetchPolicyContext<Record<string, string>, OperationVariables>'.
Type 'Record<string, string>' is not assignable to type '{ foo: string; }'
The minimal reproducible case would be this
import { WatchQueryOptions } from "@apollo/client/core/watchQueryOptions";
const optionsA: WatchQueryOptions<{}, { foo: string }> = {} as WatchQueryOptions<{}, { foo: string }>
const optionsB: WatchQueryOptions<{}, Record<string, string>> = optionsA
which is also implemented in the reproduction linked below, where the error is also indicated.
Link to Reproduction
https://codesandbox.io/p/sandbox/youthful-thompson-d48qtm
@apollo/client
version
3.10.1
This is because the return value of useQuery
uses TData
both in covariant and contravariant positions.
I'd recommend that you use TypedDocumentNode
here and then let inference take over:
const getById = <TData>(gql: TypedDocumentNode<TData>, id: string) => {
return useQuery(
gql,
computed(() => ({ id }))
)
}
and then use it like
const result = getById(MyQuery, "1")
Assuming that your queries are typed correctly (and this is probably the single most important thing to do when using GraphQL with TypeScript!), everything should be typed correctly from there on.
Your manual annotations only add inconsistencies and possibly type erasure, so I'd try to avoid those.
That said, this still breaks the rule of hooks.
What is the real value here instead of just using
const result = useQuery(gql, computed(() => ({ id })))
in your component?
Thanks for the hint, I actually forgot about the concept of contravariance, also I was not aware that there is TypedDocumentNode
.
I'm currently abstracting CRUD operations for a multitude of different entity types, where all come with their own queries and result / variables types, hence why I need some form of generic typing for - in this case - an ID-based single entity query. With your hint I got the typing down correct now though, so thanks a lot!
Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Client usage and allow us to serve you better.