TypeScript with Apollo Client


As your application grows, a type system can become an essential tool for catching bugs early and improving your overall developer experience.

GraphQL uses a type system to clearly define the available data for each type and field in a GraphQL schema. Given that a GraphQL server's schema is strongly typed, we can generate TypeScript definitions automatically using a tool like GraphQL Code Generator. We'll use our generated types to ensure type safety for the inputs and results of our GraphQL operations.

Below, we'll guide you through installing and configuring GraphQL Code Generator to generate types for your hooks and components.

Setting up your project

This article assumes your project already uses TypeScript. If not, configure your project to use TypeScript or start a new project.

To get started using GraphQL Code Generator, begin by installing the following packages (using Yarn or NPM):

Bash
1yarn add -D typescript graphql @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core

Next, we'll create a configuration file for GraphQL Code Generator, named codegen.ts, at the root of our project:

TypeScript
codegen.ts
1import { CodegenConfig } from '@graphql-codegen/cli';
2
3const config: CodegenConfig = {
4  schema: '<URL_OF_YOUR_GRAPHQL_API>',
5  // this assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
6  documents: ['src/**/*.{ts,tsx}'],
7  generates: {
8    './src/__generated__/': {
9      preset: 'client',
10      plugins: [],
11      presetConfig: {
12        gqlTagName: 'gql',
13      }
14    }
15  },
16  ignoreNoDocuments: true,
17};
18
19export default config;

There are multiple ways to specify a schema in your codegen.ts, so pick whichever way works best for your project setup.

Finally, we'll add the following scripts to our package.json file:

JSON
package.json
1{
2  "scripts": {
3    "compile": "graphql-codegen",
4    "watch": "graphql-codegen -w",
5  }
6}

Running either of the scripts above generates types based on the schema file or GraphQL API you provided in codegen.ts:

Bash
1$ yarn run compile
2 Parse Configuration
3 Generate outputs

Typing hooks

GraphQL Code Generator automatically creates a gql function (from the src/__generated__/gql.ts file). This function enables us to type the variables that go into our React hooks, along with the results from those hooks.

useQuery

Below we use the gql function to define our query, which automatically generates types for our useQuery hook:

TypeScript
1import React from 'react';
2import { useQuery } from '@apollo/client';
3
4import { gql } from '../src/__generated__/gql';
5
6const GET_ROCKET_INVENTORY = gql(/* GraphQL */ `
7  query GetRocketInventory($year: Int!) {
8    rocketInventory(year: $year) {
9      id
10      model
11      year
12      stock
13    }
14  }
15`);
16
17export function RocketInventoryList() {
18  // our query's result, data, is typed!
19  const { loading, data } = useQuery(
20    GET_ROCKET_INVENTORY,
21    // variables are also typed!
22    { variables: { year: 2019 } }
23  );
24  return (
25    <div>
26      <h3>Available Inventory</h3>
27      {loading ? (
28        <p>Loading ...</p>
29      ) : (
30        <table>
31          <thead>
32            <tr>
33              <th>Model</th>
34              <th>Stock</th>
35            </tr>
36          </thead>
37          <tbody>
38            {data && data.rocketInventory.map(inventory => (
39              <tr>
40                <td>{inventory.model}</td>
41                <td>{inventory.stock}</td>
42              </tr>
43            ))}
44          </tbody>
45        </table>
46      )}
47    </div>
48  );
49}

fetchMore and subscribeToMore

The useQuery hook returns an instance of QueryResult, which includes the fetchMore and subscribeToMore functions. See Queries for detailed type information. Because these functions execute GraphQL operations, they accept type parameters.

By default, the type parameters for fetchMore are the same as those for useQuery. Because both fetchMore and useQuery encapsulate a query operation, it's unlikely that you'll need to pass any type arguments to fetchMore.

Expanding our previous example, notice that we don't explicitly type fetchMore, because it defaults to using the same type parameters as useQuery:

TypeScript
1// ...
2export function RocketInventoryList() {
3  const { fetchMore, loading, data } = useQuery(
4    GET_ROCKET_INVENTORY,
5    // variables are typed!
6    { variables: { year: 2019 } }
7  );
8
9  return (
10    //...
11    <button
12      onClick={() => {
13        // variables are typed!
14        fetchMore({ variables: { year: 2020 } });
15      }}
16    >
17      Add 2020 Inventory
18    </button>
19    //...
20  );
21}

The type parameters and defaults for subscribeToMore are identical to those for fetchMore. Keep in mind that subscribeToMore executes a subscription, whereas fetchMore executes follow-up queries.

Using subscribeToMore, you usually pass at least one typed argument, like so:

TypeScript
1// ...
2const ROCKET_STOCK_SUBSCRIPTION = gql(/* GraphQL */ `
3  subscription OnRocketStockUpdated {
4    rocketStockAdded {
5      id
6      stock
7    }
8  }
9`);
10
11export function RocketInventoryList() {
12  const { subscribeToMore, loading, data } = useQuery(
13    GET_ROCKET_INVENTORY,
14    { variables: { year: 2019 } }
15  );
16
17  React.useEffect(() => {
18    subscribeToMore(
19      // variables are typed!
20      { document: ROCKET_STOCK_SUBSCRIPTION, variables: { year: 2019 } }
21    );
22  }, [subscribeToMore])
23
24  // ...
25}

useMutation

We can type useMutation hooks the same way we type useQuery hooks. Using the generated gql function to define our GraphQL mutations, we ensure that we type our mutation's variables and return data:

TypeScript
1import React, { useState } from 'react';
2import { useMutation } from '@apollo/client';
3
4import { gql } from '../src/__generated__/gql';
5
6const SAVE_ROCKET = gql(/* GraphQL */ `
7  mutation saveRocket($rocket: RocketInput!) {
8    saveRocket(rocket: $rocket) {
9      model
10    }
11  }
12`);
13
14
15export function NewRocketForm() {
16  const [model, setModel] = useState('');
17  const [year, setYear] = useState(0);
18  const [stock, setStock] = useState(0);
19
20  // our mutation's result, data, is typed!
21  const [saveRocket, { error, data }] = useMutation(SAVE_ROCKET, {
22    // variables are also typed!
23    variables: { rocket: { model, year: +year, stock: +stock } }
24  });
25
26  return (
27    <div>
28      <h3>Add a Rocket</h3>
29      {error ? <p>Oh no! {error.message}</p> : null}
30      {data && data.saveRocket ? <p>Saved!</p> : null}
31      <form>
32        <p>
33          <label>Model</label>
34          <input
35            name="model"
36            onChange={e => setModel(e.target.value)}
37          />
38        </p>
39        <p>
40          <label>Year</label>
41          <input
42            type="number"
43            name="year"
44            onChange={e => setYear(+e.target.value)}
45          />
46        </p>
47        <p>
48          <label>Stock</label>
49          <input
50            type="number"
51            name="stock"
52            onChange={e => setStock(e.target.value)}
53          />
54        </p>
55        <button onClick={() => model && year && stock && saveRocket()}>
56          Add
57        </button>
58      </form>
59    </div>
60  );
61}

useSubscription

We can type our useSubscription hooks the same way we typed our useQuery and useMutation hooks. Using the generated gql function to define our GraphQL subscriptions, we ensure that we type our subscription variables and return data:

TypeScript
1import React from 'react';
2import { useSubscription } from '@apollo/client';
3
4import { gql } from '../src/gql';
5
6const LATEST_NEWS = gql(/* GraphQL */ `
7  subscription getLatestNews {
8    latestNews {
9      content
10    }
11  }
12`);
13
14export function LatestNews() {
15  // our returned data is typed!
16  const { loading, data } = useSubscription(LATEST_NEWS);
17  return (
18    <div>
19      <h5>Latest News</h5>
20      <p>
21        {loading ? 'Loading...' : data!.latestNews.content}
22      </p>
23    </div>
24  );
25}

Typing Render Prop components

To type render prop components, you'll first define a GraphQL query using the generated gql function (from src/__generated__/gql).

This creates a type for that query and its variables, which you can then pass to your Query component:

TypeScript
1import { gql, AllPeopleQuery, AllPeopleQueryVariables } from '../src/__generated__/gql';
2
3const ALL_PEOPLE_QUERY = gql(/* GraphQL */ `
4  query All_People {
5    allPeople {
6      people {
7        id
8        name
9      }
10    }
11  }
12`;
13
14
15const AllPeopleComponent = <Query<AllPeopleQuery, AllPeopleQueryVariables> query={ALL_PEOPLE_QUERY}>
16  {({ loading, error, data }) => { ... }}
17</Query>

Our <Query /> component's function arguments are now typed. Since we aren't mapping any props coming into our component, nor are we rewriting the props passed down, we only need to provide the shape of our data and the variables for our typing to work!

This approach also works for <Mutation /> and <Subscription /> components.

Extending components

In previous versions of Apollo Client, render prop components (Query, Mutation and Subscription) could be extended to add additional type information:

TypeScript
1class SomeQuery extends Query<SomeData, SomeVariables> {}

Now that class-based render prop components have been converted into functional components, you can no longer extend components in this manner.

While we recommend switching over to using the new useQuery, useMutation, and useSubscription hooks as soon as possible, you can replace your class with a wrapped and typed component in the meantime:

TypeScript
1export const SomeQuery = () => (
2  <Query<SomeData, SomeVariables> query={SOME_QUERY} /* ... */>
3    {({ loading, error, data }) => { ... }}
4  </Query>
5);

Typing Higher-order components

To type higher-order components, begin by defining your GraphQL queries with the gql function (from ./src/__generated__/gql). In the below example, this generates the query and variable types (GetCharacterQuery and GetCharacterQueryVariables).

Our wrapped component receives our query's result as props, and we'll need to tell our type system the shape these props take.

Below is an example of setting types for an operation using the graphql higher-order component:

TypeScript
1import React from "react";
2import { ChildDataProps, graphql } from "@apollo/react-hoc";
3
4import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';
5
6const HERO_QUERY = gql(/* GraphQL */ `
7  query GetCharacter($episode: Episode!) {
8    hero(episode: $episode) {
9      name
10      id
11      friends {
12        name
13        id
14        appearsIn
15      }
16    }
17  }
18`);
19
20
21type ChildProps = ChildDataProps<{}, GetCharacterQuery, GetCharacterQueryVariables>;
22
23// Note that the first parameter here is an empty Object, which means we're
24// not checking incoming props for type safety in this example. The next
25// example (in the "Options" section) shows how the type safety of incoming
26// props can be ensured.
27const withCharacter = graphql<{}, GetCharacterQuery, GetCharacterQueryVariables, ChildProps>(HERO_QUERY, {
28  options: () => ({
29    variables: { episode: "JEDI" }
30  })
31});
32
33export default withCharacter(({ data: { loading, hero, error } }) => {
34  if (loading) return <div>Loading</div>;
35  if (error) return <h1>ERROR</h1>;
36  return ...// actual component with data;
37});

The following logic also works for query, mutation, and subscription higher-order components!

Options

Typically, our wrapper component's props pass in a query's variables. Wherever our application uses our wrapper component, we want to ensure that we correctly type those passed-in arguments.

Below is an example of setting a type for a component's props:

TypeScript
1import React from "react";
2import { ChildDataProps, graphql } from "@apollo/react-hoc";
3
4import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';
5
6const HERO_QUERY = gql(/* GraphQL */ `
7  query GetCharacter($episode: Episode!) {
8    hero(episode: $episode) {
9      name
10      id
11      friends {
12        name
13        id
14        appearsIn
15      }
16    }
17  }
18`);
19
20type ChildProps = ChildDataProps<GetCharacterQueryVariables, GetCharacterQuery, GetCharacterQueryVariables>;
21
22const withCharacter = graphql<
23  GetCharacterQueryVariables,
24  GetCharacterQuery,
25  GetCharacterQueryVariables,
26  ChildProps
27>(HERO_QUERY, {
28  options: ({ episode }) => ({
29    variables: { episode }
30  }),
31});
32
33export default withCharacter(({ data: { loading, hero, error } }) => {
34  if (loading) return <div>Loading</div>;
35  if (error) return <h1>ERROR</h1>;
36  return ...// actual component with data;
37});

This is especially helpful when accessing deeply nested objects passed to our component via props. For example, when adding prop types, a project using TypeScript begins to surface errors with invalid props:

TypeScript
1import React from "react";
2import {
3  ApolloClient,
4  createHttpLink,
5  InMemoryCache,
6  ApolloProvider
7} from "@apollo/client";
8
9import Character from "./Character";
10
11export const link = createHttpLink({
12  uri: "https://mpjk0plp9.lp.gql.zone/graphql"
13});
14
15export const client = new ApolloClient({
16  cache: new InMemoryCache(),
17  link,
18});
19
20export default () =>
21  <ApolloProvider client={client}>
22    // $ExpectError property `episode`. Property not found in. See: src/Character.js:43
23    <Character />
24  </ApolloProvider>;

Props

The props function enables you to manually reshape an operation result's data into the shape your wrapped component requires:

TypeScript
1import React from "react";
2import { graphql, ChildDataProps } from "@apollo/react-hoc";
3
4import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';
5
6const HERO_QUERY = gql(/* GraphQL */ `
7  query GetCharacter($episode: Episode!) {
8    hero(episode: $episode) {
9      name
10      id
11      friends {
12        name
13        id
14        appearsIn
15      }
16    }
17  }
18`);
19
20
21type ChildProps = ChildDataProps<GetCharacterQueryVariables, GetCharacterQuery, GetCharacterQueryVariables>;
22
23const withCharacter = graphql<
24  GetCharacterQueryVariables,
25  GetCharacterQuery,
26  GetCharacterQueryVariables,
27  ChildProps
28>(HERO_QUERY, {
29  options: ({ episode }) => ({
30    variables: { episode }
31  }),
32  props: ({ data }) => ({ ...data })
33});
34
35export default withCharacter(({ loading, hero, error }) => {
36  if (loading) return <div>Loading</div>;
37  if (error) return <h1>ERROR</h1>;
38  return ...// actual component with data;
39});

Above, we type the shape of our response, props, and our client's variables. Our options and props function (within the graphql wrapper) are now type-safe, our rendered component is protected, and our tree of components has their required props enforced:

TypeScript
1export const withCharacter = graphql<
2  GetCharacterQueryVariables,
3  GetCharacterQuery,
4  GetCharacterQueryVariables,
5  Props
6>(HERO_QUERY, {
7  options: ({ episode }) => ({
8    variables: { episode }
9  }),
10  props: ({ data, ownProps }) => ({
11    ...data,
12    // $ExpectError [string] This type cannot be compared to number
13    episode: ownProps.episode > 1,
14    // $ExpectError property `isHero`. Property not found on object type
15    isHero: data && data.hero && data.hero.isHero
16  })
17});

Classes vs functions

If you are using React classes (instead of using the graphql wrapper), you can still type the incoming props for your class like so:

TypeScript
1import { ChildProps } from "@apollo/react-hoc";
2
3const withCharacter = graphql<GetCharacterQueryVariables, GetCharacterQuery>(HERO_QUERY, {
4  options: ({ episode }) => ({
5    variables: { episode }
6  })
7});
8
9class Character extends React.Component<ChildProps<GetCharacterQueryVariables, GetCharacterQuery>, {}> {
10  render(){
11    const { loading, hero, error } = this.props.data;
12    if (loading) return <div>Loading</div>;
13    if (error) return <h1>ERROR</h1>;
14    return ...// actual component with data;
15  }
16}
17
18export default withCharacter(Character);

Using the name property

If you are using the name property in the configuration of the graphql wrapper, you need to manually attach the type of the response to the props function, like so:

TypeScript
1import { NamedProps, QueryProps } from '@apollo/react-hoc';
2
3export const withCharacter = graphql<GetCharacterQueryVariables, GetCharacterQuery, {}, Prop>(HERO_QUERY, {
4  name: 'character',
5  props: ({ character, ownProps }: NamedProps<{ character: QueryProps & GetCharacterQuery }, Props) => ({
6    ...character,
7    // $ExpectError [string] This type cannot be compared to number
8    episode: ownProps.episode > 1,
9    // $ExpectError property `isHero`. Property not found on object type
10    isHero: character && character.hero && character.hero.isHero
11  })
12});

Using TypedDocumentNode

In TypeScript, all APIs that intake DocumentNode can alternatively take TypedDocumentNode<Data, Variables>. This type has the same JavaScript representation but enables APIs to infer the data and variable types (instead of making you specify types upon invocation).

This technique enables us to modify the useQuery example above to use a type inference:

TypeScript
1import React from 'react';
2import { useQuery, gql, TypedDocumentNode } from '@apollo/client';
3
4interface RocketInventoryData {
5  rocketInventory: RocketInventory[];
6}
7
8interface RocketInventoryVars {
9  year: number;
10}
11
12const GET_ROCKET_INVENTORY: TypedDocumentNode<RocketInventoryData, RocketInventoryVars> = gql`
13  query GetRocketInventory($year: Int!) {
14    rocketInventory(year: $year) {
15      id
16      model
17      year
18      stock
19    }
20  }
21`;
22
23export function RocketInventoryList() {
24  const { loading, data } = useQuery(
25    GET_ROCKET_INVENTORY,
26    { variables: { year: 2019 } }
27  );
28  return (
29    <div>
30      <h3>Available Inventory</h3>
31      {loading ? (
32        <p>Loading ...</p>
33      ) : (
34        <table>
35          <thead>
36            <tr>
37              <th>Model</th>
38              <th>Stock</th>
39            </tr>
40          </thead>
41          <tbody>
42            {data && data.rocketInventory.map(inventory => (
43              <tr>
44                <td>{inventory.model}</td>
45                <td>{inventory.stock}</td>
46              </tr>
47            ))}
48          </tbody>
49        </table>
50      )}
51    </div>
52  );
53}
Feedback

Edit on GitHub

Forums