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
}
}
]
})
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 }>
]
})
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 }>
]
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With