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/coreNext, 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 outputsTyping 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/react";
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 : <table>
30 <thead>
31 <tr>
32 <th>Model</th>
33 <th>Stock</th>
34 </tr>
35 </thead>
36 <tbody>
37 {data &&
38 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(GET_ROCKET_INVENTORY, {
13 variables: { year: 2019 },
14 });
15
16 React.useEffect(() => {
17 subscribeToMore(
18 // variables are typed!
19 { document: ROCKET_STOCK_SUBSCRIPTION, variables: { year: 2019 } }
20 );
21 }, [subscribeToMore]);
22
23 // ...
24}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/react";
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
14export function NewRocketForm() {
15 const [model, setModel] = useState("");
16 const [year, setYear] = useState(0);
17 const [stock, setStock] = useState(0);
18
19 // our mutation's result, data, is typed!
20 const [saveRocket, { error, data }] = useMutation(SAVE_ROCKET, {
21 // variables are also typed!
22 variables: { rocket: { model, year: +year, stock: +stock } },
23 });
24
25 return (
26 <div>
27 <h3>Add a Rocket</h3>
28 {error ?
29 <p>Oh no! {error.message}</p>
30 : null}
31 {data && data.saveRocket ?
32 <p>Saved!</p>
33 : null}
34 <form>
35 <p>
36 <label>Model</label>
37 <input name="model" onChange={(e) => setModel(e.target.value)} />
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/react";
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>{loading ? "Loading..." : data!.latestNews.content}</p>
21 </div>
22 );
23}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 { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client";
3import { ApolloProvider } from "@apollo/client/react";
4
5import Character from "./Character";
6
7export const link = createHttpLink({
8 uri: "https://mpjk0plp9.lp.gql.zone/graphql",
9});
10
11export const client = new ApolloClient({
12 cache: new InMemoryCache(),
13 link,
14});
15
16export default () => (
17 <ApolloProvider client={client}>
18 // $ExpectError property `episode`. Property not found in. See:
19 src/Character.js:43
20 <Character />
21 </ApolloProvider>
22);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 { gql, TypedDocumentNode } from "@apollo/client";
3import { useQuery } from "@apollo/client/react";
4
5interface RocketInventoryData {
6 rocketInventory: RocketInventory[];
7}
8
9interface RocketInventoryVars {
10 year: number;
11}
12
13const GET_ROCKET_INVENTORY: TypedDocumentNode<
14 RocketInventoryData,
15 RocketInventoryVars
16> = gql`
17 query GetRocketInventory($year: Int!) {
18 rocketInventory(year: $year) {
19 id
20 model
21 year
22 stock
23 }
24 }
25`;
26
27export function RocketInventoryList() {
28 const { loading, data } = useQuery(GET_ROCKET_INVENTORY, {
29 variables: { year: 2019 },
30 });
31 return (
32 <div>
33 <h3>Available Inventory</h3>
34 {loading ?
35 <p>Loading ...</p>
36 : <table>
37 <thead>
38 <tr>
39 <th>Model</th>
40 <th>Stock</th>
41 </tr>
42 </thead>
43 <tbody>
44 {data &&
45 data.rocketInventory.map((inventory) => (
46 <tr>
47 <td>{inventory.model}</td>
48 <td>{inventory.stock}</td>
49 </tr>
50 ))}
51 </tbody>
52 </table>
53 }
54 </div>
55 );
56}Data masking
Learn more about integrating TypeScript with data masking in the data masking docs.