Produce type-safe GraphQL queries using TypeScript 💥

Zevi Reinitz - Jan 12 '23 - - Dev Community

TL;DR

In this tutorial, you'll learn how to produce type-safe GraphQL queries using TypeScript using some great tools, techniques, and solutions.

Intro

When you're building a company obsessed with making front-end teams' lives better, it's only natural to spend a lot of time thinking about front-end type things.

And that explains a lot about my team.

But we figured that instead of just daydreaming, we'd try to turn our random thought process into something more productive and useful for people like you. 😅

We were recently reflecting on how much front end developers love GQL. It also occurred to us that they also love TypeScript.

So... how amazing would it be if we could combine them?

Livecycle thinking

As it turns out, there are a bunch of tools that can help developers produce type-safe GraphQL queries using TypeScript. In this article, we will explore the cream-of-the-crop tools, techniques, and solutions that can help create a great experience for developers.

Livecycle - the best way for front end devs to collaborate & get PRs reviewed... fast.

[Now, just some quick background on who we are, so you'll understand why we're daydreaming about this kind of thing.

Livecycle is a contextual collaboration tool for front-end development teams. We know how painful it is trying to get multiple stakeholders to review changes before they go live (unclear comments... context switches... different time zones... too many meetings... zero coordination... you know exactly what I'm talking about 🙄) so we built a solution.

Our SDK turns any PR deploy preview environment into a collaborative playground where the team can leave visual review comments in context. By facilitating a contextual, async review we save frontend teams tons of time, money and headaches. (If your team is building a product together - check out how Livecycle can help)]

And now back to our regular scheduled program... Buckle up.

Autocomplete for Writing Queries

Let's start by exploring some techniques for showing autocomplete types when writing queries. Autocomplete is a nice feature that allows developers to see the available schema options for fields, arguments, types, and variables as they write queries. It should work with .graphql files and ggl template strings as well.

figure1

Fig 1: Autocomplete in Action

The editor that you use will determine the process of setting up autocomplete in TypeScript as well as the method of applying the correct configuration.

For example, say that you have a GraphQL endpoint in http://localhost:10008/graphql and you want to enable autocomplete for it. You need to introspect the schema and use it to fill the autocomplete options.

Introspecting the Schema

You need to enable the editor to match the schema and show the allowed fields and types as you write. This is easy if you use Webstorm with the GraphQL plugin. Just click on the tab to configure the endpoint that you want to introspect, and it will generate a graphql-config file that looks something like this:

figure2

Fig 2: The Webstorm GraphQL Plugin



// ./graphql-config.json
{
  "name": "MyApp",
  "schemaPath": "schema.graphql",
  "extensions": {
    "endpoints": {
      "Default GraphQL Endpoint": {
        "url": "http://localhost:10008/graphql",
        "headers": {
          "user-agent": "JS GraphQL"
        },
        "introspect": true
      }
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

Once this step is finished, you will be able to use autocomplete when writing ggl and .graphql queries.

You can also do this using VSCode with the VSCode GraphQL plugin. You may want to download the schema file first:



npm i get-graphql-schema -g
get-graphql-schema http://localhost:10008/graphql > schema.graphql


Enter fullscreen mode Exit fullscreen mode

Then, if you use the previous graphql-config.json file, everything should work as expected there as well.

Generating Types Automatically from Gql/Graphql Files

Once you have the autocomplete feature enabled, you should use TypeScript to return valid types for the result data when you write ggl files.

In this case, you need to generate TypeScript types from the GraphQL schema so that you can use the TypeScript Language Server to autocomplete fields in code.

First, you use a tool called @graphql-codegen to perform the schema -> TypeScript types generation. Install the required dependencies like this:



npm i @graphql-codegen/introspection \
  @graphql-codegen/TypeScript @graphql-codegen/TypeScript-operations \
  @graphql-codegen/cli --save-dev


Enter fullscreen mode Exit fullscreen mode

Then, create a codegen.yml file that contains the code generation configuration:



# ./codegen.yml
overwrite: true
schema: "http://localhost:10008/graphql"
documents: "pages/\*\*/\*.graphql"
generates:
  types/generated.d.ts:
    plugins:
      - typescript
      - typescript-operations
      - introspection


Enter fullscreen mode Exit fullscreen mode

Just add the location of the schema endpoint, the documents to scan for queries, and the location of the generated files to this file.

For example, we have a posts.graphql file with the following contents:



# ./posts.graphql
query GetPostList {
  posts {
    nodes {
      excerpt
      id
      databaseId
      title
      slug
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

Then, we add this task in package.json and run it:



// package.json

"scripts": {

/*
...
*/

"generate": "graphql-codegen --config codegen.yml",

}


Enter fullscreen mode Exit fullscreen mode


npm run generate


Enter fullscreen mode Exit fullscreen mode

This will create ambient types in types/generated.d.ts. Now we can use them in queries:



import postsQuery from "./posts.graphql"
import { GetPostListQuery } from "../types/generated"

const [response] = useQuery<GetPostListQuery>({ query: postsQuery })


Enter fullscreen mode Exit fullscreen mode

Note: we're able to load .graphql files using import statements with the following webpack rule:



{
      test: /\.(graphql|gql)$/,
      exclude: /node_modules/,
      loader: 'graphql-tag/loader',
 }


Enter fullscreen mode Exit fullscreen mode

Now, the response data will be properly typechecked when you access the relevant fields even if you don’t type anything:

figure3

Fig 3: Using Generated Types in TypeScript

You can also watch the .graphql files and automatically generate the appropriate types without going back and running the same commands again by adding the -w flag. That way, the types will always be in sync as you update them:



//package.json

"scripts": {

//…

"generate": "graphql-codegen --config codegen.yml -w,

}



Enter fullscreen mode Exit fullscreen mode

Better type inference using typed-document-node

The GraphQL Code Generator project offers a variety of plugins that make it possible to provide a better development experience with Typescript and GraphQL. One of those plugins is the typed-document-node which allows developers to avoid importing the .graphql file and instead use a generated Typescript type for the query result. As you type the result of the operation you just requested you get automatic type inference, auto-complete and type checking.

To use it first you need to install the plugin itself:



npm i @graphql-typed-document-node/core  \
  @graphql-codegen/typed-document-node --save-dev


Enter fullscreen mode Exit fullscreen mode

Then include it in the codegen.yml file:



# ./codegen.yml

overwrite: true
schema: "http://localhost:10008/graphql"
documents: "pages/\*\*/*.graphql"
generates:
  types/generated.d.ts:
    plugins:
    - typescript
    - typescript-operations
    - typed-document-node
    - introspection


Enter fullscreen mode Exit fullscreen mode

Now run the generate command again to create the new TypedDocumentNode which extends the DocumentNode interface. This will update the types/generated.d.ts file that includes now the following types:



export type GetPostListQueryVariables = Exact<{ [key: string]: never; }>;
export type GetPostListQuery = 
export const GetPostListDocument = 


Enter fullscreen mode Exit fullscreen mode

Now instead of providing the generic type parameter in the query, you just use the GetPostListDocument constant and remove the .graphql import, which allows Typescript to infer the types of the result data. So we can change the index.tsx to be:

index.tsx



import { GetPostListDocument } from "../types/generated.d";


const result = useQuery(GetPostListDocument);


Enter fullscreen mode Exit fullscreen mode

You can query the response data which will infer the types for you as you type:

figure4

Overall this plugin greatly improves the development experience when working with GraphQL queries.

Data Fetching Libraries for Web and Node/Deno

Finally, let’s explore the best data fetching libraries for Web and Node/Deno. There are three main contenders:

Graphql-Request

This is a minimalist GraphQL client, and it’s the simplest way to call the endpoint aside from calling it directly using fetch. You can use it to perform direct queries to the GraphQL endpoint without any advanced features like cache strategies or refetching. However, that does not stop you from leveraging the autocomplete and typed queries features:



npm i graphql-request --save


Enter fullscreen mode Exit fullscreen mode


// ./getPosts.tsx

import { gql } from "@apollo/client"
import { request } from "graphql-request"
import { GetPostListQuery } from "../types/generated"

export const getPosts = async () => {
  const data = await request<GetPostListQuery>(
    "http://localhost:10003",
    gql`
        query GetPostList {
            posts {
                nodes {
                    excerpt
                    id
                    databaseId
                    title
                    slug
                }
            }
        }
    `
  )

  return data?.posts?.nodes?.slice() ?? []
}


Enter fullscreen mode Exit fullscreen mode

For example, you can use the request function to perform a direct query to the GraphQL endpoint and then pass on the generated GetPostListQuery type. While you are typing the query, you’ll see the autocomplete capability, and you can also see that the response data is typed correctly. Alternatively, if you do not want to pass on the endpoint every time, you can use the GraphQLClient class instead:



import { GraphQLClient } from "graphql-request"
import { GetPostListQuery } from "../types/generated"

const client = new GraphQLClient("http://localhost:10003")

export const getPosts = async () => {
  const data = await client.request
}


Enter fullscreen mode Exit fullscreen mode

Apollo Client

This is one of the oldest and most feature-complete clients. It offers many useful production grade capabilities like cache strategies, refetching, and integrated states.

Getting started with Apollo Client is relatively simple. You just need to configure a client and pass it around the application:



npm install @apollo/client graphql


Enter fullscreen mode Exit fullscreen mode


// client.ts

import { ApolloClient, InMemoryCache } from "@apollo/client"

const client = new ApolloClient({
  uri: "http://localhost:10003/graphql",
  cache: new InMemoryCache(),
  connectToDevTools: true,
})


Enter fullscreen mode Exit fullscreen mode

You want to specify the uri to connect to, the cache mechanism to use, and a flag to connect to the dev tools plugin. Then, provide the client with the query or mutation parameters to perform queries:



// ./getPosts.tsx

import client from "./client"
import { gql } from "@apollo/client"
import { GetPostListQuery } from "../types/generated"

export const getPosts = async () => {
  const { data } = await client.query<GetPostListQuery>({
    query: gql`
          query GetPostList {
              posts {
                  nodes {
                      excerpt
                      id
                      databaseId
                      title
                      slug
                  }
              }
          }
      `,
  })

  return data?.posts?.nodes?.slice() ?? []
}


Enter fullscreen mode Exit fullscreen mode

If you’re using React, we recommend that you connect the client with the associated provider, as follows:



// ./index.tsx

import { ApolloProvider } from "@apollo/client"
import client from "./client"

const App = () => (
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
)


Enter fullscreen mode Exit fullscreen mode

Overall, Apollo is a stable and feature-complete client that will cater to all of your needs. It’s a good choice.

Urql

This is a more modern client from Formidable Labs. It’s highly customizable and very flexible by default. Urql leverages the concept of exchanges, which are like middleware functions that enhance its functionality. It also comes with its own dev tools extension and offers many add-ons. You begin the setup process (which is very similar to those discussed above) as follows:



npm install --save @urql/core graphql


Enter fullscreen mode Exit fullscreen mode


// ./client.ts

import { createClient, defaultExchanges } from "urql"
import { devtoolsExchange } from "@urql/devtools"

const client = createClient({
  url: "http://localhost:10003/graphql",

  exchanges: [devtoolsExchange, ...defaultExchanges],
})


Enter fullscreen mode Exit fullscreen mode

queries.gql



# ./graphql/queries.gql

query GetPostList {
  posts {
    nodes {
      excerpt
      id
      databaseId
      title
      slug
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

You need to specify the url to connect to and the list of exchanges to use. Then, you can perform queries by calling the query method with the client and then converting it to a promise using the toPromise method:



// ./index.ts

import client from "./client"
import { GetPostDocument } from "../types/generated"

async function getPosts() {
  const { data } = await client.query(GetPostDocument).toPromise()
  return data
}


Enter fullscreen mode Exit fullscreen mode

Overall, Urql can be a good alternative to Apollo, as it is highly extensible and provides a good development experience.

In addition, if we're using React app, we can install the @graphql-codegen/typescript-urql plugin and generate higher order components or hooks for easily consuming data from our GraphQL server.

Bottom line & Next Steps with GraphQL and TypeScript

At the end of the day, we just want to make developers happy. So we genuinely hope that you enjoyed reading this article.

For further reading, you can explore more tools in the GraphQL ecosystem by looking through the awesome-graphql list.

Check us out!

If you found this article useful, then you're probably the kind of human who would appreciate the value Livecycle can bring to front end teams.

I'd be thrilled if you installed our SDK on one of your projects and gave it a spin with your team. 🙏

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .