Automatically generate TypeScript definitions for your GraphQL request with graphql-codegen
If you ever had the chance to work with GraphQL requests in your Frontend using tools such as Apollo or React-Query, you often have to deal with untyped requests, which can make you lose a lot of the power that TypeScript has. The easy solution for that is to manually type the response, but that can be a pain in the ass to do and surely doesn't scale well. This article will try to fix that by providing you a way to automatically generate the type from a GraphQL response using the graphql-codegen.
Initial considerations:
- We'll be using
React
as the client library, but at the end of the article I'll be leaving some resources with alternatives likeVue
,Angular
, andSvelte
- For the GraphQL client, we'll be using
Apollo
, but the resources at the end also has alternatives such asReact Query
andURQL
- Regarding package management, I'll be defaulting to
yarn
, but you're free to use the alternatives such asnpm
orpnpm
.
Setting up the application:
For our example, we'll be creating a simple React application that will render a list of products. The list will be fetched from the mock.shop API from Shopify. If you don't feel like coding along, feel free to check out the final version of the code here.
Now let's first set up our app using Vite. To do that, use the following commands:
yarn create vite mock-product-list --template react-ts
cd mock-product-list
yarn
yarn dev
This will generate and run a simple React + TypeScript app. I'll go ahead and delete most of the content to create just a Hello World. Leaving it like this:
Setting up Apollo consuming mock.shop API
To do that, let's follow Apollo's get started guide.
First, run:
yarn add @apollo/client graphql
In my main.tsx
I'll add:
import { StrictMode } from 'react'
import ReactDOM from 'react-dom/client'
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'
import App from './App'
const client = new ApolloClient({
uri: 'https://mock.shop/api',
cache: new InMemoryCache(),
})
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<ApolloProvider client={client}>
<StrictMode>
<App />
</StrictMode>
</ApolloProvider>
)
Now, on the App.tsx
file, we can get the first 10 products using gql
and useQuery
import { gql, useQuery } from '@apollo/client'
const GET_PRODUCTS = gql`
query GetProducts {
products(first: 10) {
edges {
node {
id
title
description
createdAt
}
}
}
}
`
function App() {
const { loading, data } = useQuery(GET_PRODUCTS)
if (loading) {
return <p>Loading...</p>
}
console.log({ data })
return <h1>Hello world</h1>
}
export default App
We have set up Apollo fetching data from the GraphQL API:
Add TypeScript definitions to the GraphQL response
Now comes the problem: the data
variable is being typed as any
:
One common solution for this, is to manually type the useQuery
using the generic provided by the variable.
type ProductsType = {
edges: {
node: {
id: string
title: string
createdAt: string
description: string
}
}[]
}
function App() {
// data now has the type ProductsType
const { loading, data } = useQuery<ProductsType>(GET_PRODUCTS)
if (loading) {
return <p>Loading...</p>
}
console.log({ data })
return <h1>Hello world</h1>
}
This can solve the problem, however it doesn't scale really well since it's a very manual and error-prone process. Every time you make a change on the request, you need to manually change the type being used.
If only there was a way to automatically do that, oh wait, there is! And this is what this article is about. Let's make that happen.
Automatically setting TypeScript types for the GET_PRODUCTS
request
First, we need to install the graphql-codegen libraries. In your terminal, run:
yarn add -D @graphql-codegen/cli @graphql-codegen/client-preset
And create a codegen.ts
file on your root folder with the following content:
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'https://mock.shop/api',
documents: ['src/**/*.tsx'],
ignoreNoDocuments: true, // for better experience with the watcher
generates: {
'./src/gql/': {
preset: 'client',
},
},
}
export default config
Now, on a separated terminal instance, you need to start GraphQL Code Generator:
yarn graphql-codegen --watch
This will automatically generate a src/gql
folder. I recommend adding it to the .gitignore
file to ensure this is re-generated for everyone that runs the project:
The next step now is to just change the GET_PRODUCTS
variable to use the generated graphql()
function generated in src/gql
. Your App.tsx
should look like this:
import { useQuery } from '@apollo/client'
import { graphql } from './gql'
const GET_PRODUCTS = graphql(`
query GetProducts {
products(first: 10) {
edges {
node {
id
title
description
createdAt
}
}
}
}
`)
function App() {
const { loading, data } = useQuery(GET_PRODUCTS)
if (loading) {
return <p>Loading...</p>
}
console.log({ data })
return <h1>Hello world</h1>
}
export default App
Now let's see what type data has:
And looks like it works! The type is now automatically generated. But are the types actually generated based on the fields we requested? I created a quick video to showcase it:
Looks like it works! YAY!
Last time hint
Maybe you noticed that to have both the codegen and your server running, you'll need 2 terminal instances, however if you want to parallelize this work into a single terminal you can use the concurrently lib. Just go on your terminal to add the lib:
yarn add -D concurrently
And instead of running yarn dev
, run:
yarn concurrently "yarn vite" "yarn graphql-codegen --watch"
That way you can both the http server and the graphql-codegen watch in the same command.
That's all folks.
Now you should be able to automatically type any GraphQL requests you make from now on, and no longer you will need to go through the tedious work of manually typing then. Hope you find any value in this article for your existing and future projects using GraphQL.
Resources:
- https://www.apollographql.com/docs/react/get-started/
- https://the-guild.dev/graphql/codegen/docs/getting-started
Feel free follow me on my social medias if you want:
See you next time!