Using Apollo with TypeScript


As your application grows, you may find it helpful to include a type system to assist in development. Apollo supports type definitions for TypeScript out of the box. Both apollo-client and React Apollo ship with definitions in their npm associated packages, so installation should be done for you after the libraries are included in your project.

These docs assume you already have TypeScript configured in your project, if not start here.

The most common need when using type systems with GraphQL is to type the results of an operation. Given that a GraphQL server's schema is strongly typed, we can even generate TypeScript definitions automatically using a tool like apollo-codegen. In these docs however, we will be writing result types manually.

Typing hooks

React Apollo's useQuery, useMutation and useSubscription hooks are fully typed, and Generics can be used to type both incoming operation variables and GraphQL result data. React Apollo Hook options and result types are listed in the Hooks API section of the docs. You can find a typed example of each Hook below.

useQuery

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

useMutation

JavaScript
1import React, { useState } from 'react';
2import { useMutation } from '@apollo/react-hooks';
3import gql from 'graphql-tag';
4
5const SAVE_ROCKET = gql`
6  mutation saveRocket($rocket: RocketInput!) {
7    saveRocket(rocket: $rocket) {
8      model
9    }
10  }
11`;
12
13interface RocketInventory {
14  id: number;
15  model: string;
16  year: number;
17  stock: number;
18}
19
20interface NewRocketDetails {
21  model: string;
22  year: number;
23  stock: number;
24}
25
26export function NewRocketForm() {
27  const [model, setModel] = useState('');
28  const [year, setYear] = useState(0);
29  const [stock, setStock] = useState(0);
30
31  const [saveRocket, { error, data }] = useMutation<
32    { saveRocket: RocketInventory },
33    { rocket: NewRocketDetails }
34  >(SAVE_ROCKET, {
35    variables: { rocket: { model, year: +year, stock: +stock } }
36  });
37
38  return (
39    <div>
40      <h3>Add a Rocket</h3>
41      {error ? <p>Oh no! {error.message}</p> : null}
42      {data && data.saveRocket ? <p>Saved!</p> : null}
43      <form>
44        <p>
45          <label>Model</label>
46          <input
47            name="model"
48            onChange={e => setModel(e.target.value)}
49          />
50        </p>
51        <p>
52          <label>Year</label>
53          <input
54            type="number"
55            name="model"
56            onChange={e => setYear(+e.target.value)}
57          />
58        </p>
59        <p>
60          <label>Stock</label>
61          <input
62            type="number"
63            name="stock"
64            onChange={e => setStock(e.target.value)}
65          />
66        </p>
67        <button onClick={() => model && year && stock && saveRocket()}>
68          Add
69        </button>
70      </form>
71    </div>
72  );
73}

useSubscription

JavaScript
1import React from 'react';
2import { useSubscription } from '@apollo/react-hooks';
3import gql from 'graphql-tag';
4
5interface Message {
6  content: string;
7}
8
9interface News {
10  latestNews: Message;
11}
12
13const LATEST_NEWS = gql`
14  subscription getLatestNews {
15    latestNews {
16      content
17    }
18  }
19`;
20
21export function LatestNews() {
22  const { loading, data } = useSubscription<News>(LATEST_NEWS);
23  return (
24    <div>
25      <h5>Latest News</h5>
26      <p>
27        {loading ? 'Loading...' : data!.latestNews.content}
28      </p>
29    </div>
30  );
31}

Typing Render Prop Components

Using Apollo together with TypeScript couldn't be easier than using it with component API released in React Apollo 2.1:

TypeScript
1const ALL_PEOPLE_QUERY = gql`
2  query All_People_Query {
3    allPeople {
4      people {
5        id
6        name
7      }
8    }
9  }
10`;
11
12interface Data {
13  allPeople: {
14    people: Array<{ id: string; name: string }>;
15  };
16};
17
18interface Variables {
19  first: number;
20};
21
22const AllPeopleComponent = <Query<Data, Variables> query={ALL_PEOPLE_QUERY}>
23  {({ loading, error, data }) => { ... }}
24</Query>

Now the <Query /> component render prop function arguments are typed. Since we are not 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 full typing to work! Everything else is handled by React Apollo's robust type definitions.

This approach is the exact same for the <Query />, <Mutation />, and <Subcription /> components! Learn it once, and get the best types ever with Apollo.

Extending components

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

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

Since all class based render prop components have been converted to functional components, extending components in this manner is no longer possible. While we recommend switching over to use the new useQuery, useMutation and useSubscription hooks as soon as possible, if you're looking for a stop gap you can consider replacing your class with a wrapped and typed component:

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

Typing Higher Order Components

Since the result of a query will be sent to the wrapped component as props, we want to be able to tell our type system the shape of those props. Here is an example setting types for an operation using the graphql higher order component (note: the follow sections also work for the query, mutation, and subscription hocs):

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

Options

Typically, variables to the query will be computed from the props of the wrapper component. Wherever the component is used in your application, the caller would pass arguments that we want our type system to validate what the shape of these props could look like. Here is an example setting the type of props:

TypeScript
1import React from "react";
2import gql from "graphql-tag";
3import { ChildDataProps, graphql } from "react-apollo";
4
5const HERO_QUERY = gql`
6  query GetCharacter($episode: Episode!) {
7    hero(episode: $episode) {
8      name
9      id
10      friends {
11        name
12        id
13        appearsIn
14      }
15    }
16  }
17`;
18
19type Hero = {
20  name: string;
21  id: string;
22  appearsIn: string[];
23  friends: Hero[];
24};
25
26type Response = {
27  hero: Hero;
28};
29
30type InputProps = {
31  episode: string;
32};
33
34type Variables = {
35  episode: string;
36};
37
38type ChildProps = ChildDataProps<InputProps, Response, Variables>;
39
40const withCharacter = graphql<InputProps, Response, Variables, ChildProps>(HERO_QUERY, {
41  options: ({ episode }) => ({
42    variables: { episode }
43  }),
44});
45
46export default withCharacter(({ data: { loading, hero, error } }) => {
47  if (loading) return <div>Loading</div>;
48  if (error) return <h1>ERROR</h1>;
49  return ...// actual component with data;
50});

This is especially helpful when accessing deeply nested objects that are passed down to the component through props. For example, when adding prop types, a project using TypeScript will begin to surface errors where props being passed are invalid:

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

Props

One of the most powerful feature of the React integration is the props function which allows you to reshape the result data from an operation into a new shape of props for the wrapped component. GraphQL is awesome at allowing you to only request the data you want from the server. The client still often needs to reshape or do client side calculations based on these results. The return value can even differ depending on the state of the operation (i.e loading, error, recieved data), so informing our type system of choice of these possible values is really important to make sure our components won't have runtime errors.

The graphql wrapper from react-apollo supports manually declaring the shape of your result props.

TypeScript
1import React from "react";
2import gql from "graphql-tag";
3import { graphql, ChildDataProps } from "react-apollo";
4
5const HERO_QUERY = gql`
6  query GetCharacter($episode: Episode!) {
7    hero(episode: $episode) {
8      name
9      id
10      friends {
11        name
12        id
13        appearsIn
14      }
15    }
16  }
17`;
18
19type Hero = {
20  name: string;
21  id: string;
22  appearsIn: string[];
23  friends: Hero[];
24};
25
26type Response = {
27  hero: Hero;
28};
29
30type InputProps = {
31  episode: string
32};
33
34type Variables = {
35  episode: string
36};
37
38type ChildProps = ChildDataProps<InputProps, Response, Variables>;
39
40const withCharacter = graphql<InputProps, Response, Variables, ChildProps>(HERO_QUERY, {
41  options: ({ episode }) => ({
42    variables: { episode }
43  }),
44  props: ({ data }) => ({ ...data })
45});
46
47export default withCharacter(({ loading, hero, error }) => {
48  if (loading) return <div>Loading</div>;
49  if (error) return <h1>ERROR</h1>;
50  return ...// actual component with data;
51});

Since we have typed the response shape, the props shape, and the shape of what will be passed to the client, we can prevent errors in multiple places. Our options and props function within the graphql wrapper are now type safe, our rendered component is protected, and our tree of components have their required props enforced.

TypeScript
1export const withCharacter = graphql<InputProps, Response, Variables, Props>(HERO_QUERY, {
2  options: ({ episode }) => ({
3    variables: { episode }
4  }),
5  props: ({ data, ownProps }) => ({
6    ...data,
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: data && data.hero && data.hero.isHero
11  })
12});

With this addition, the entirety of the integration between Apollo and React can be statically typed. When combined with the strong tooling each system provides, it can make for a much improved application and developer experience.

Classes vs Functions

All of the above examples show wrapping a component which is just a function using the result of a graphql wrapper. Sometimes, components that depend on GraphQL data require state and are formed using the class MyComponent extends React.Component practice. In these use cases, TypeScript requires adding prop shape to the class instance. In order to support this, react-apollo exports types to support creating result types easily.

TypeScript
1import { ChildProps } from "react-apollo";
2
3const withCharacter = graphql<InputProps, Response>(HERO_QUERY, {
4  options: ({ episode }) => ({
5    variables: { episode }
6  })
7});
8
9class Character extends React.Component<ChildProps<InputProps, Response>, {}> {
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 will need to manually attach the type of the response to the props function. An example using TypeScript would be like this:

TypeScript
1import { NamedProps, QueryProps } from 'react-apollo';
2
3export const withCharacter = graphql<InputProps, Response, {}, Prop>(HERO_QUERY, {
4  name: 'character',
5  props: ({ character, ownProps }: NamedProps<{ character: QueryProps & Response }, 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});
Feedback

Edit on GitHub

Forums