GraphQL + TypeScript === 💖

GraphQL + TypeScript === 💖

We love GQL. We also love TypeScript. What if we could combine them?

As it turns out, there are a plethora 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.

Let’s get started.

Autocomplete for Writing Queries

We’ll 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.

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:

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
      }
    }
  }
}

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

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

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

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
    }
  }
}

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

// package.json

"scripts": {

/*
...
*/

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

}
npm run generate

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 })

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',
 }

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

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,

}

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

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

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 =

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);

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

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
// ./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() ?? []
}

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
}

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
// client.ts

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

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

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() ?? []
}

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>
)

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
// ./client.ts

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

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

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

queries.gql

# ./graphql/queries.gql

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

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
}

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.

Next Steps with GraphQL and TypeScript

We hope that you enjoyed reading this article as much as we enjoyed writing it!

You can explore more tools in the GraphQL ecosystem by looking through the awesome-graphql list. Until then, happy hacking!

Livecycle

Livecycle

February 01, 2022

Let's build
better, together.

Join the Livecycle community.

Stay in the loop.

Get product updates that may actually change your life (as a developer). Spam not included.