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 like Vue, Angular, and Svelte
  • For the GraphQL client, we'll be using Apollo, but the resources at the end also has alternatives such as React Query and URQL
  • Regarding package management, I'll be defaulting to yarn, but you're free to use the alternatives such as npm or pnpm.

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:

VSCode setup + Hello world app

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:

Application showing the responde from GraphQL in the console

Add TypeScript definitions to the GraphQL response

Now comes the problem: the data variable is being typed as any:

Screenshot of the IDE showing data type 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: gql folder showing in the src folder

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: data variable with a proper type

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:

Feel free follow me on my social medias if you want:

See you next time!