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!