apollographql/apollo-client

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.