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