GraphQL with TypeScript done right

Charly Poly

Generics, and Mapped types, are key to build types on top of existing ones by making them configurable (Generics) or iterables (Mapped types).

Advanced types give to your code and open-source libraries the power of providing an API that manipulates data (your application objects) without breaking the “types chain”.

The best TypeScript configuration ensures that the “types chain” is continuous

The TypeScript “Types Chain”

TypeScript helps with typing data and following how the data is used and transformed by subsequent functions or method calls.

The example below shows how easily this “types chain” can be broken:

const a = '1' // a is a string
 
const stringToInt = (num: string): any => parseInt(num, 10)
 
const b = stringToInt('5') // b is of type any

How to break the TypeScript “types chain” (playground demo)

Since React 16.8 brought ubiquitous functional components, a React application can be seen as a mix of functional components dealing with state and data in order to provide UI to the users.

Like with plain JavaScript functions, the same rules of the “types chain” apply to your React application that will look to something similar to the following:

/blog-assets/typescript-with-graphql-done-right/typescript-with-graphql-done-right-1.png

Most modern React applications have the following data setup: centralized data store passed down to components through contexts, transformed by custom hooks down to UI components.

Since React applications are built on top of data, we can conclude that:

The strength of your React application’s types is based on the stability of your data types

The Flawed “Handwritten” Data Types

Most React projects type remote data (from APIs) manually, either at the component level with interfaces or in a global dedicated .d.ts file.

interface User {
  id: string
  email: string
}
 
interface Chat {
  id: string
  user: User
  messages: Message[]
}
 
//…
 
const userQuery = gql`
  query currentUser {
    me {
      id
      email
    }
  }
`
 
const Login = () => {
  const { data } = useQuery(userQuery)
  const user = data ? (data.me as User) : null
  // ...
}

Example of data-types definition and linked usage, common in many projects

Manually writing and maintaining those type can lead to human errors:

  • outdated typing (regarding the API current implementation)
  • typos
  • partial typing of data (not all API’s data has a corresponding type)

As we saw earlier, the strength of your React TypeScript types is based on your data types, therefore, any mistake on your manually maintained data types will ripple in many of your React components.

/blog-assets/typescript-with-graphql-done-right/typescript-with-graphql-done-right-2.png

In our hypothetical application, the User type has some typos that will impact the stability of the associated components at runtime, defecting the benefits of TypeScript.

Fortunately, thanks to the GraphQL introspection feature, many tools arose to solve this problem by providing data-types - and even more - generation tools.

Robust React Application’s Types with GraphQL

GraphQL Code Generator, given the mutations and queries used by the app and the access to the target GraphQL API, generates the corresponding TypeScript types.

/blog-assets/typescript-with-graphql-done-right/typescript-with-graphql-done-right-3.png

GraphQL Code Generator is doing all the heavy lifting by getting from the API the definitions of the data types used by the React applications queries and mutations.

Let’s see an example with our hypothetical application Login component relying on the User type.

Stronger Generated TypeScript Types

First, let’s create a queries.graphql file in a src/graphql folder:

queries.graphql
query currentUser {
  me {
    id
    email
  }
}

then, the following GraphQL Code Generator configuration at the root of our project:

schema: http://localhost:3000/graphql
documents: ./src/graphql/*.graphql
generates:
  graphql/generated.ts:
    plugins:
      - typescript-operations
      - typescript-react-apollo
  config:
    withHooks: false

codegen.yml

And after running graphql-codegen CLI, we can refactor our <Login> component:

import { currentUserDocument, CurrentUserQueryResult } from '../graphql/generated.ts'
 
// no need to create the User type or `gql` query, we import them from the generated file
const Login = () => {
  const { data } = useQuery<CurrentUserQueryResult>(currentUserDocument)
  // user is typed!
  const user = data ? data.me : null
 
  // ...
}

src/components/Login.tsx

The configuration and refactoring were straightforward, directly impacting our data types, which are now directly linked to the GraphQL API Schema, making our React application more stable!

Contrary to the manually maintained data types, using the GraphQL Code Generator puts the data-types maintenance on the GraphQL API side.

Maintaining data types on the front-end side only consist of running the GraphQL Code Generator tool to update types according to the last GraphQL API version.

Let’s now see some more advanced configurations that bring more stability.

Getting the Most of Your GraphQL Code Generator Configuration

When used with React Apollo Client, GraphQL Code Generator offers three main configuration modes:

Generate TypeScript types definitions

This is the configuration that we used in our previous example:

schema: http://localhost:3000/graphql
documents: ./src/graphql/*.graphql
generates:
  graphql/generated.ts:
    plugins:
      - typescript-operations
      - typescript-react-apollo
config:
  withHooks: false

codegen.yml

This configuration will generate a src/graphql/generated.ts file that will contain:

  • GraphQL document nodes
  • TypeScript Query/Mutation Result types (return type of our GraphQL operations)
  • TypeScript Query/Mutation Variables types (variables types of our GraphQL operations)

Here an example of GraphQL Code Generator output given our previous currentUser Query:

import { gql } from '@apollo/client'
import * as Apollo from '@apollo/client'
 
export type CurrentUserQueryVariables = Exact<{ [key: string]: never }>
export type CurrentUserQuery = { __typename?: 'Query' } & {
  me: { __typename?: 'User' } & Pick<User, 'id'>
}
 
export const CurrentUserDocument = gql`
  query currentUser {
    me {
      id
    }
  }
`
 
export type CurrentUserQueryResult = Apollo.QueryResult<CurrentUserQuery, CurrentUserQueryVariables>

src/graphql/generated.ts

We already saw the benefits of these generated types on the <Login> component refactoring.

However, we can agree that having to provide both the query TypeScript type (CurrentUserQueryResult) and the query GraphQL document node (currentUserDocument) to useQuery() is cumbersome: useQuery<CurrentUserQueryResult>(currentUserDocument)

Let’s see how we can improve that in the next configuration mode.

Generate Typed React Hooks

GraphQL Code Generator is capable of more than just generating TypeScript types, it can also generate JavaScript/TypeScript code.

Let’s see how we can ask GraphQL Code Generator to generate Typed React hooks, so we don’t have to provide the TypeScript types to useQuery() every time.

Let’s use the following configuration:

schema: http://localhost:3000/graphql
documents: ./src/graphql/*.graphql
generates:
  graphql/generated.ts:
    plugins:
      - typescript-operations
      - typescript-react-apollo

codegen.yml

This configuration will generate a src/graphql/generated.ts file that will contain:

  • GraphQL document node
  • TypeScript Query/Mutation Result types (return type of our GraphQL operations)
  • TypeScript Query/Mutation Variables types (variables types of our GraphQL operations)
  • One custom hook for each defined GraphQL operation

Example given our previous currentUser Query:

import { gql } from '@apollo/client'
import * as Apollo from '@apollo/client'
 
const defaultOptions = {}
export type CurrentUserQueryVariables = Exact<{ [key: string]: never }>
export type CurrentUserQuery = { __typename?: 'Query' } & {
  me: { __typename?: 'User' } & Pick<User, 'id'>
}
 
export const CurrentUserDocument = gql`
  query currentUser {
    me {
      id
    }
  }
`
 
export function useCurrentUserQuery(
  baseOptions?: Apollo.QueryHookOptions<CurrentUserQuery, CurrentUserQueryVariables>
) {
  const options = { ...defaultOptions, ...baseOptions }
  return Apollo.useQuery<CurrentUserQuery, CurrentUserQueryVariables>(CurrentUserDocument, options)
}
export type CurrentUserQueryHookResult = ReturnType<typeof useCurrentUserQuery>
export type CurrentUserQueryResult = Apollo.QueryResult<CurrentUserQuery, CurrentUserQueryVariables>

src/graphql/generated.ts

Which will give us this updated version of our <Login> component:

import { useCurrentUserQuery } from '../graphql/generated.ts'
 
// no need to create the User type or `gql` query, we import them from the generated file
 
const Login = () => {
  const { data } = useCurrentUserQuery()
  // user is typed!
  const user = data ? data.me : null
 
  // ...
}

src/components/Login.tsx

Nice! Isn’t it?

Generate Typed Documents

GraphQL Code Generator is providing another simple way to use typed GraphQL Query and Mutations, called TypedDocumentNode.

With the following configuration:

schema: http://localhost:3000/graphql
documents: ./src/graphql/*.graphql
generates:
  graphql/generated.ts:
    plugins:
      - typescript-operations
      - typed-document-node

codegen.yml

GraphQL Code Generator will generate the following file:

import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
 
export type CurrentUserQueryVariables = Exact<{ [key: string]: never }>
export type CurrentUserQuery = { __typename?: 'Query' } & {
  me: { __typename?: 'User' } & Pick<User, 'id'>
}
 
export const CurrentUserDocument: DocumentNode<CurrentUserQuery, CurrentUserQueryVariables> = {
  kind: 'Document',
  definitions: [
    {
      kind: 'OperationDefinition',
      operation: 'query',
      name: { kind: 'Name', value: 'currentUser' },
      selectionSet: {
        kind: 'SelectionSet',
        selections: [
          {
            kind: 'Field',
            name: { kind: 'Name', value: 'me' },
            selectionSet: {
              kind: 'SelectionSet',
              selections: [{ kind: 'Field', name: { kind: 'Name', value: 'id' } }]
            }
          }
        ]
      }
    }
  ]
}

src/graphql/generated.ts

This allows us the following refactoring of our <Login> component:

import { CurrentUserDocument } from '../graphql/generated.ts'
 
// no need to create the User type or `gql` query, we import them from the generated file
 
const Login = () => {
  const { data } = useQuery(CurrentUserDocument)
  // user is typed!
  const user = data ? data.me : null
 
  // ...
}

src/components/Login.tsx

In my experience, it is more scalable to go for the TypedDocumentNode approach instead of the hooks generation.

The generation of one custom hook per GraphQL operation (Query/Mutation) can generate a LOT of hooks at scale along with a lot of imports, which is not necessary given the useMutation() useQuery provided by Apollo Client.

Tips: Leverage GraphQL Fragments for Scalable Types

Now that we have many ways to generate **stable **data types, let’s see how to make them easier to use and maintain in time.

Let’s take a look at the following helper:

import { CurrentUserQuery } from "src/graphql/generated";
 
const isUserEmailValid = (user: CurrentUserQuery["me']) => !!user.email

Here, instead of using our currentUser query CurrentUserQuery[“me”] type, we would prefer to rely on a User type.

We can achieve this with zero maintainability by leveraging GraphQL Fragments.

When Fragments are provided, GQL Code Generator will produce the corresponding TypeScript types.

Here is our updated src/graphql/queries.graphql:

query currentUser {
  me {
    ...User
  }
}

The ...User indicates to GraphQL that we want to expand our User fragment here, similar to the object spread syntax.

In order to do so, we need to provide to GraphQL Code Generator the definition of the User fragment that we will place in a new src/graphql/fragments.graphql file:

fragment User on users {
   id
   email
}

src/graphql/fragments.graphql

Please note that a fragment needs to be defined against an existing type of the GraphQL API Schema, here users.

Here is our updated helper code:

import { UserFragment } from 'src/graphql/generated'
 
const isUserEmailValid = (user: UserFragment) => !!user.email

Leveraging GraphQL Fragments allows you to build your React app data types on top of the GraphQL API types.

Please note that multiple fragments can be defined on a single GraphQL Schema type:

fragment User on users {
  id
  email
}
fragment UserProfile on users {
  id
  email
  firstName
  lastName
}

src/graphql/fragments.graphql

A good practice is to ensure that all your Query and Mutations responses use fragment, this will ensure that your React application can benefit from well-defined data types of different specificity, ex:

  • User type carries the necessary base properties
  • UserProfile type carries the minimum user info for display
  • UserExtended type carries all the users properties

Conclusion

The TypeScript type system is powerful and valuable only if used properly.

In React applications, most of the components rely on data, doing your data typing at the center of your application stability.

Thanks to GraphQL Code Generator and with a fast setup, you will be able to ensure the stability of your React application data types, along with your application’s global stability.

If you decide to use GraphQL Code Generator, make sure to:

  • move all your gql definitions in dedicated .graphql files
  • Favor the TypedDocumentNode configuration mode
  • Make sure that all your Queries and Mutations relies on well-defined GraphQL Fragments

Join our newsletter

Want to hear from us when there's something new?
Sign up and stay up to date!

*By subscribing, you agree with Beehiiv’s Terms of Service and Privacy Policy.