Docs
Launch GraphOS Studio

TypeScript with Apollo Client


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

uses a to clearly define the available data for each type and in a . Given that a '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 .

Below, we'll guide you through installing and configuring 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 Code Generator, begin by installing the following packages (using Yarn or NPM):

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

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

codegen.ts
import { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: '<URL_OF_YOUR_GRAPHQL_API>',
// this assumes that all your source files are in a top-level `src/` directory - you might need to adjust this to your file structure
documents: ['src/**/*.{ts,tsx}'],
generates: {
'./src/__generated__/': {
preset: 'client',
plugins: [],
presetConfig: {
gqlTagName: 'gql',
}
}
},
ignoreNoDocuments: true,
};
export 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:

package.json
{
"scripts": {
"compile": "graphql-codegen",
"watch": "graphql-codegen -w",
}
}

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

$ yarn run compile
✔ Parse Configuration
✔ Generate outputs

Typing hooks

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

useQuery

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

import React from 'react';
import { useQuery } from '@apollo/client';
import { gql } from '../src/__generated__/gql';
const GET_ROCKET_INVENTORY = gql(/* GraphQL */ `
query GetRocketInventory($year: Int!) {
rocketInventory(year: $year) {
id
model
year
stock
}
}
`);
export function RocketInventoryList() {
// our query's result, data, is typed!
const { loading, data } = useQuery(
GET_ROCKET_INVENTORY,
// variables are also typed!
{ variables: { year: 2019 } }
);
return (
<div>
<h3>Available Inventory</h3>
{loading ? (
<p>Loading ...</p>
) : (
<table>
<thead>
<tr>
<th>Model</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
{data && data.rocketInventory.map(inventory => (
<tr>
<td>{inventory.model}</td>
<td>{inventory.stock}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}

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 , 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 , it's unlikely that you'll need to pass any type 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:

// ...
export function RocketInventoryList() {
const { fetchMore, loading, data } = useQuery(
GET_ROCKET_INVENTORY,
// variables are typed!
{ variables: { year: 2019 } }
);
return (
//...
<button
onClick={() => {
// variables are typed!
fetchMore({ variables: { year: 2020 } });
}}
>
Add 2020 Inventory
</button>
//...
);
}

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 , like so:

// ...
const ROCKET_STOCK_SUBSCRIPTION = gql(/* GraphQL */ `
subscription OnRocketStockUpdated {
rocketStockAdded {
id
stock
}
}
`);
export function RocketInventoryList() {
const { subscribeToMore, loading, data } = useQuery(
GET_ROCKET_INVENTORY,
{ variables: { year: 2019 } }
);
React.useEffect(() => {
subscribeToMore(
// variables are typed!
{ document: ROCKET_STOCK_SUBSCRIPTION, variables: { year: 2019 } }
);
}, [subscribeToMore])
// ...
}

useMutation

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

import React, { useState } from 'react';
import { useMutation } from '@apollo/client';
import { gql } from '../src/__generated__/gql';
const SAVE_ROCKET = gql(/* GraphQL */ `
mutation saveRocket($rocket: RocketInput!) {
saveRocket(rocket: $rocket) {
model
}
}
`);
export function NewRocketForm() {
const [model, setModel] = useState('');
const [year, setYear] = useState(0);
const [stock, setStock] = useState(0);
// our mutation's result, data, is typed!
const [saveRocket, { error, data }] = useMutation(SAVE_ROCKET, {
// variables are also typed!
variables: { rocket: { model, year: +year, stock: +stock } }
});
return (
<div>
<h3>Add a Rocket</h3>
{error ? <p>Oh no! {error.message}</p> : null}
{data && data.saveRocket ? <p>Saved!</p> : null}
<form>
<p>
<label>Model</label>
<input
name="model"
onChange={e => setModel(e.target.value)}
/>
</p>
<p>
<label>Year</label>
<input
type="number"
name="year"
onChange={e => setYear(+e.target.value)}
/>
</p>
<p>
<label>Stock</label>
<input
type="number"
name="stock"
onChange={e => setStock(e.target.value)}
/>
</p>
<button onClick={() => model && year && stock && saveRocket()}>
Add
</button>
</form>
</div>
);
}

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 , we ensure that we type our subscription and return data:

import React from 'react';
import { useSubscription } from '@apollo/client';
import { gql } from '../src/gql';
const LATEST_NEWS = gql(/* GraphQL */ `
subscription getLatestNews {
latestNews {
content
}
}
`);
export function LatestNews() {
// our returned data is typed!
const { loading, data } = useSubscription(LATEST_NEWS);
return (
<div>
<h5>Latest News</h5>
<p>
{loading ? 'Loading...' : data!.latestNews.content}
</p>
</div>
);
}

Typing Render Prop components

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

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

import { gql, AllPeopleQuery, AllPeopleQueryVariables } from '../src/__generated__/gql';
const ALL_PEOPLE_QUERY = gql(/* GraphQL */ `
query All_People {
allPeople {
people {
id
name
}
}
}
`;
const AllPeopleComponent = <Query<AllPeopleQuery, AllPeopleQueryVariables> query={ALL_PEOPLE_QUERY}>
{({ loading, error, data }) => { ... }}
</Query>

Our <Query /> component's function 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 for our typing to work!

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

Extending components

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

class 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:

export const SomeQuery = () => (
<Query<SomeData, SomeVariables> query={SOME_QUERY} /* ... */>
{({ loading, error, data }) => { ... }}
</Query>
);

Typing Higher-order components

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

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

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

import React from "react";
import { ChildDataProps, graphql } from "@apollo/react-hoc";
import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';
const HERO_QUERY = gql(/* GraphQL */ `
query GetCharacter($episode: Episode!) {
hero(episode: $episode) {
name
id
friends {
name
id
appearsIn
}
}
}
`);
type ChildProps = ChildDataProps<{}, GetCharacterQuery, GetCharacterQueryVariables>;
// Note that the first parameter here is an empty Object, which means we're
// not checking incoming props for type safety in this example. The next
// example (in the "Options" section) shows how the type safety of incoming
// props can be ensured.
const withCharacter = graphql<{}, GetCharacterQuery, GetCharacterQueryVariables, ChildProps>(HERO_QUERY, {
options: () => ({
variables: { episode: "JEDI" }
})
});
export default withCharacter(({ data: { loading, hero, error } }) => {
if (loading) return <div>Loading</div>;
if (error) return <h1>ERROR</h1>;
return ...// actual component with data;
});

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

Options

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

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

import React from "react";
import { ChildDataProps, graphql } from "@apollo/react-hoc";
import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';
const HERO_QUERY = gql(/* GraphQL */ `
query GetCharacter($episode: Episode!) {
hero(episode: $episode) {
name
id
friends {
name
id
appearsIn
}
}
}
`);
type ChildProps = ChildDataProps<GetCharacterQueryVariables, GetCharacterQuery, GetCharacterQueryVariables>;
const withCharacter = graphql<
GetCharacterQueryVariables,
GetCharacterQuery,
GetCharacterQueryVariables,
ChildProps
>(HERO_QUERY, {
options: ({ episode }) => ({
variables: { episode }
}),
});
export default withCharacter(({ data: { loading, hero, error } }) => {
if (loading) return <div>Loading</div>;
if (error) return <h1>ERROR</h1>;
return ...// actual component with data;
});

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:

import React from "react";
import {
ApolloClient,
createHttpLink,
InMemoryCache,
ApolloProvider
} from "@apollo/client";
import Character from "./Character";
export const link = createHttpLink({
uri: "https://mpjk0plp9.lp.gql.zone/graphql"
});
export const client = new ApolloClient({
cache: new InMemoryCache(),
link,
});
export default () =>
<ApolloProvider client={client}>
// $ExpectError property `episode`. Property not found in. See: src/Character.js:43
<Character />
</ApolloProvider>;

Props

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

import React from "react";
import { graphql, ChildDataProps } from "@apollo/react-hoc";
import { gql, GetCharacterQuery, GetCharacterQueryVariables } from '../src/gql';
const HERO_QUERY = gql(/* GraphQL */ `
query GetCharacter($episode: Episode!) {
hero(episode: $episode) {
name
id
friends {
name
id
appearsIn
}
}
}
`);
type ChildProps = ChildDataProps<GetCharacterQueryVariables, GetCharacterQuery, GetCharacterQueryVariables>;
const withCharacter = graphql<
GetCharacterQueryVariables,
GetCharacterQuery,
GetCharacterQueryVariables,
ChildProps
>(HERO_QUERY, {
options: ({ episode }) => ({
variables: { episode }
}),
props: ({ data }) => ({ ...data })
});
export default withCharacter(({ loading, hero, error }) => {
if (loading) return <div>Loading</div>;
if (error) return <h1>ERROR</h1>;
return ...// actual component with data;
});

Above, we type the shape of our response, props, and our client's . 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:

export const withCharacter = graphql<
GetCharacterQueryVariables,
GetCharacterQuery,
GetCharacterQueryVariables,
Props
>(HERO_QUERY, {
options: ({ episode }) => ({
variables: { episode }
}),
props: ({ data, ownProps }) => ({
...data,
// $ExpectError [string] This type cannot be compared to number
episode: ownProps.episode > 1,
// $ExpectError property `isHero`. Property not found on object type
isHero: data && data.hero && data.hero.isHero
})
});

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:

import { ChildProps } from "@apollo/react-hoc";
const withCharacter = graphql<GetCharacterQueryVariables, GetCharacterQuery>(HERO_QUERY, {
options: ({ episode }) => ({
variables: { episode }
})
});
class Character extends React.Component<ChildProps<GetCharacterQueryVariables, GetCharacterQuery>, {}> {
render(){
const { loading, hero, error } = this.props.data;
if (loading) return <div>Loading</div>;
if (error) return <h1>ERROR</h1>;
return ...// actual component with data;
}
}
export 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:

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

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 types (instead of making you specify types upon invocation).

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

import React from 'react';
import { useQuery, gql, TypedDocumentNode } from '@apollo/client';
interface RocketInventoryData {
rocketInventory: RocketInventory[];
}
interface RocketInventoryVars {
year: number;
}
const GET_ROCKET_INVENTORY: TypedDocumentNode<RocketInventoryData, RocketInventoryVars> = gql`
query GetRocketInventory($year: Int!) {
rocketInventory(year: $year) {
id
model
year
stock
}
}
`;
export function RocketInventoryList() {
const { loading, data } = useQuery(
GET_ROCKET_INVENTORY,
{ variables: { year: 2019 } }
);
return (
<div>
<h3>Available Inventory</h3>
{loading ? (
<p>Loading ...</p>
) : (
<table>
<thead>
<tr>
<th>Model</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
{data && data.rocketInventory.map(inventory => (
<tr>
<td>{inventory.model}</td>
<td>{inventory.stock}</td>
</tr>
))}
</tbody>
</table>
)}
</div>
);
}
Previous
Developer tools
Next
Testing React components
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company