Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript type for Apollo useMutation's refetchQueries?

I have an Apollo useMutation hook with TypeScript types:

const MY_MUTATION = gql`
  mutation($source: String!) {
    doSomething(source: $source) {
      result
    }
  }
`

const [myMutation] = useMutation<ReturnPayloadType, MutationVariables>(MY_MUTATION);

Which gives me type safety when I use it:

myMutation({
  variables: {
    source: 'some source',
    // foo: 'bar' This would error if uncommented. 
  }
})

This is working but now I need to use the refetchQueries key. How can I add types to it?

myMutation({
  variables: {
    source: 'some source',
    // foo: 'bar' This would error if uncommented. 
  },
  refetchQueries: [
    {
      query: MY_OTHER_QUERY,
      variables: {
        foo: 'bar' // I want to add type safety here
      }
    }
  ]
})
like image 930
Evanss Avatar asked Sep 11 '25 18:09

Evanss


1 Answers

Short answer

You can't really. As of Apollo Client v3.4.0 refetchQueries is typed so that it eventually resolves to:

| "all"
| "active"
|  Array<string | DocumentNode | QueryOptions>`

The typings don't have a type parameter (generic) to allow you to specify your own typing for refetchQueries. I would rely on GraphQL's type guards at runtime and potentially type casting as a minor guard at compile time. You can cast the type of each object passed to refetchQueries to QueryOptions and specify the type of the variables via QueryOption's type parameter. Type casting does have the caveat that you need to remember to use it and also it does not guarantee the types of the variables and query will match.

myMutation({
  variables: {
    source: 'some source',
    // foo: 'bar' This would error if uncommented. 
  },
  refetchQueries: [
    {
      query: MY_OTHER_QUERY,
      variables: {
        foo: 'bar'
      }
    } as QueryOptions<{ foo: string }>
  ]
})

Long answer

As of Apollo Client v3.4.0 the refetchQueries option is typed as follows:

  refetchQueries?:
    | ((result: FetchResult<TData>) => InternalRefetchQueriesInclude)
    | InternalRefetchQueriesInclude;

– https://github.com/apollographql/apollo-client/blob/release-3.4/src/core/watchQueryOptions.ts#L223-L225

where InternalRefetchQueriesInclude is typed as:

export type InternalRefetchQueriesInclude =
  | InternalRefetchQueryDescriptor[]
  | RefetchQueriesIncludeShorthand;

– https://github.com/apollographql/apollo-client/blob/release-3.4/src/core/types.ts#L35-L37

and then the above essentially resolves to:

| "all"
| "active"
|  Array<string | DocumentNode | QueryOptions>`

– https://github.com/apollographql/apollo-client/blob/release-3.4/src/core/types.ts#L26-L29

QueryOptions itself is typed here.

Throughout these types there is no type parameter (generic) to allow you to specify your own typing for refetchQueries. Although even if generics were available you'd need to solve the problem of trying to create a type that enforces "given an object with a property query of type we need another property variables with another type". Why is this important? Well, to answer that you need to ask what is the point of typing this? You want to ensure that if you use a given query any variables passed in that are the wrong type are caught at compile time instead of runtime and the risk of users encountering a bug is reduced. With functions it's easy because the identifier of the function and the identifier of the type are the same (it's built-in to functions that the type says "given this function it should have arguments of type") but in our case we don't have a function. As a result that's why I opened my answer with "you can't really".

I did explore the possibility of leveraging typeof as follows:

interface MyQueryNameRefetch extends QueryOptions {
  query: typeof MY_QUERY;
  variables: QueryVariables
}

In my original testing I only used const strings and incorrectly extrapolated the effect to the above possibility. Further testing showed this does not have the effect we want as only literal types and enum members don't widen their types when declared with or asserted to a const.

The closest you can get is typing the variables:

  refetchQueries: [
    {
      query: MY_OTHER_QUERY,
      variables: {
        foo: 'bar'
      }
    } as QueryOptions<{ foo: string }>
  ]

Notes on Arvid's answer

At this point I want to touch on @Arvid's answer. There's a function:

function typedQuery<TVariables>(
  options: QueryFunctionOptions<unknown, TVariables> & {
    query: TypedDocumentNode<unknown, TVariables>;
  },
) {
  return options;
}

And you'd use it like this:

function MyComponent() {
  const [myMutation] = useMutation<MyMutationPayloadType, MyMutationVariables>(MY_MUTATION);

  myMutation({
    variables: {
      source: 'a',
    },
    refetchQueries: [
      typedQuery<MyQueryVariables>({
        query: MY_QUERY,
        variables: {
          value: 1,
          // value: 'a' <-- gives compiler error
        },
      }),
    ],
  });
}

It's very similar to casting but it actually isn't a good idea. It shares the downsides of casting (you have to remember to use it); as well as additional ones such as having a runtime effect as the function will exist in compiled code as well as the call stack (your types should not have a runtime effect) and an effect on how the code is understood (functions convey some unit of runtime functionality but there is no runtime functionality here).

The root cause, again, comes down to the problem I described earlier about how the identifier and types are disconnected.

like image 173
Wing Avatar answered Sep 14 '25 09:09

Wing