
Although the actual API of GraphQL is typed, unfortunately there is no automaticity in that data retrieved on the client is also typed.
There have long been tools such as e.g. GraphQL Codegen which automatically generates types based on an existing GraphQL API. My experience is that such tools require a lot of setup with packages that need to be installed and scripts that need to be added, and are difficult to get to work exactly as you want.
The framework gql.tada works in a completely different way, and is written as a TypeScript plugin (new in version 5) that automatically fetches the types from the GraphQL API when TypeScript is compiled. It means that you as a developer get a "set it and forget it" experience, and personally I have almost forgotten that gql.tada is installed because it just works.
In addition, you choose which queries and mutations are to be typed automatically or not, which makes it easier to use on an existing project without having to fix thousands of errors first.
We adopted the tool on the frontend of Mist, a web application written in React that uses @apollo/client to communicate with a GraphQL API exposed by a backend. Here is a simplified example of the code for an existing query that retrieves a paginated list of users and displays them in a table:
import { gql, useQuery } from '@apollo/client'
import { AllUsersQuery, AllUsersQueryInput } from 'modules/users/types'
const ALL_USERS_QUERY = gql`
query AllUsers(
$q: String
$orderBy: String
) {
allUsers(
q: $q
orderBy: $orderBy
) {
edges {
node {
id
username
email
firstName
lastName
phone
}
}
}
}
`
const { data } = useQuery<AllUsersQuery, AllUsersQueryInput>(
ALL_USERS_QUERY,
{
variables: {
q: debouncedSearch,
orderBy: sort
},
}
)
const tableData = data.allUsers.edges.map(({ node }) => node).map(user => {
return {
id: user.id,
data: {
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
phone: parseInt(user.phone)
},
}
})
return (
<TableComponent data={tableData} />
)
This is code that probably seems familiar to most people who have worked with GraphQL in React before. In contrast, changes in User - the resource from the API is not automatically reflected in the code. It creates challenges if you forget to maintain the types AllUsersQuery and AllUsersQueryInput when a change occurs.
If you e.g. removes firstName and lastName to introduce a new field name instead, this code will not throw any error messages on it, but all the GraphQL queries will start to fail because the requested fields no longer exist in the schema. And if phone suddenly is returned as one Int , will parseInt -function will fail, because it expects an argument of type String .
Let's install gql.tada and use it to automatically follow the types from the API. We followed the starter guide on the official website, which at the time of writing involves:
gql.tada package.gql.tada/ts-plugin under compilerOptions.plugins in your existing tsconfig.json, then configure its schema and generated-types output.Then we can rewrite the code above to get rid of the static types entirely:
import { useQuery } from '@apollo/client'
import { graphql } from 'gql.tada';
const ALL_USERS_QUERY = graphql(`
query AllUsers {
allUsers {
edges {
node {
id
username
email
firstName
lastName
phone
}
}
}
}
`)
const { data } = useQuery(
ALL_USERS_QUERY,
{ ... }
)
const tableData = data.allUsers.edges.map(({ node }) => node).map(user => {
return {
id: user.id,
data: {
username: user.username,
email: user.email,
firstName: user.firstName, // TS2339: Property firstName does not exist on type
lastName: user.lastName, // TS2339: Property lastName does not exist on type
phone: parseInt(user.phone) // TS2345: Argument of type `number` is not assignable to parameter of type `string`
},
}
})
return (
<TableComponent data={tableData} />
)
Now both of the issues mentioned above are picked up by the TypeScript compiler before the code is even built. If firstName and lastName has been removed, we will get an error stating that user.firstName and user.lastName no longer exists. And correspondingly with phone : Now we will get an error message that the function expects one String , but that the type is Number .
In other files that used the static types, we can instead replace them with the built-in utility types ResultOf<typeof ALL_USERS_QUERY> and VariablesOf<typeof ALL_USERS_QUERY> . To encourage reuse even more, one can extract the requested fields into a fragment, and then use this to define how User -type looks like.
After turning on typing on more and more queries in the existing Mist code, errors started to appear that had unfortunately been overlooked before. This was mostly wrong about data potentially being returned as null or undefined , but in one case we discovered a different implementation of an enum on the frontend and backend. This probably wouldn't have been discovered organically by us developers, so there gql.tada saved us from a potential (angry) customer email!
Finally, I would like to mention some less positive experiences I have had after using gql.tada for a while.
My biggest challenge is that type safety only works on the fetched data, and not while writing the actual queries or mutations. That is, all code inside graphql() -the wrapper must be written as plain text, and all the potential errors that entails. With the caveat that I've configured something wrong, one could argue that this goes a bit outside the tool's original mission. In any case, there are separate GraphQL plugins for the biggest IDEs that take care of this task, not to mention GraphiQL or other GraphQL clients. But it would have been nice to have everything in one place!
If you use pipelines to build and roll out code, it must be mentioned that you have to check in and include the auto-generated env.d.ts the type file for the code to be built on the server. If you use an external API, or do not develop the backend yourself, it is often the case that changes appear in this file that have nothing to do with the code you are writing right now, which can confuse pull requests. In addition, the file size is relatively large, in our case it is over 1 MB.
It is also worth reading through the documentation, especially if you use fragments. These are not automatically typed, and you have to call the function readFragment() for the types from the fragment to be used. And if you have defined your own scalars in the API, e.g. one ID or Date , you will have to override the default configuration and import graphql() -wrapper from there so that these are correctly typed.
No tool is perfect, but all in all, I think the utility gql.tada delivers makes it worth trying out, especially when the effort is so low. It is an exciting tool that has already helped us in development, and which we will continue to use on Mist and other projects in the future.
From MVP prototypes to scalable platforms, our full-stack dev team turns your roadmap into rock-solid code. Get to market faster without sacrificing quality.
Get started