Typing API's data
Chapter 4: TypeScript Architecture
Chapters - Table of contents
Most modern React applications have the following setup: centralized data store passed down to components through contexts (redux, react-apollo, mobx, recoil, ...), transformed by custom hooks down to UI components.
Since React applications are built on top of data, we can conclude that:
The strength of your React application’s types is based on the stability of your data types
As we saw in the introduction, maintaining manually those types can lead to multiple issues:
  • outdated typing (regarding the API current implementation)
  • typos
  • partial typing of data (not all API’s data has a corresponding type)
Those issues, due to the modern React applications architecture will ripples through all the components.

GraphQL at the rescue

GraphQL APIs are, by design, introspectable, which means that we can get information about the types available in the schema.
GraphQL Code Generator, leverages GraphQL introspection by accessing the target GraphQL API, and generates the TypeScript types corresponding to the queries and mutations used by the React application.
GraphQL Code Generator is doing all the heavy lifting by getting from the API the definitions of the data types used by the React applications queries and mutations.
Let’s see an example with our hypothetical application <Login> component relying on the User type:
1interface User {
2  id: string;
3  email: string;
4}
5
6interface Chat {
7   id: string;
8   user: User;
9   messages: Message[]
10}
11
12//…
13
14const userQuery = gql`
15   query currentUser {
16      me {
17         id
18         email
19      }
20   }
21`
22
23const Login = () => {
24   const { data } = useQuery(userQuery)
25   const user = data ? (data.me as User) : null
26   // ...
27}
28

Stronger generated TypeScript types

First, let’s create a queries.graphql file in a src/graphql folder:
1query currentUser {
2   me {
3      id
4      email
5   }
6}
7
then, the following GraphQL Code Generator configuration (codegen.yml) at the root of our project:
1schema: http://localhost:3000/graphql
2documents: ./src/graphql/*.graphql
3generates:
4  graphql/generated.ts:
5    plugins:
6      - typescript-operations
7      - typescript-react-apollo
8  config:
9     withHooks: false
10
And, after running graphql-codegen CLI, we can refactor our <Login> component:
1import {
2  currentUserDocument,
3  CurrentUserQueryResult
4} from "../graphql/generated.ts"
5
6// no need to create the User type or `gql` query,
7//   we import them from the generated file
8const Login = () => {
9   const { data } = useQuery<CurrentUserQueryResult>(currentUserDocument)
10   // user is typed!
11   const user = data ? data.me : null
12
13   // ...
14}
15
The configuration and refactoring were straightforward, directly impacting our data types, which are now directly linked to the GraphQL API Schema, making our React application more stable!
Contrary to the manually maintained data types, using the GraphQL Code Generator puts the data-types maintenance on the GraphQL API side.
Maintaining data types on the front-end side only consist of running the GraphQL Code Generator tool to update types according to the last GraphQL API version.Let’s now see some more advanced configurations that bring more stability.

Getting the most of your GraphQL Code Generator configuration

When used with React Apollo Client, GraphQL Code Generator offers three main configuration modes:

Generate TypeScript types definitions

This is the configuration that we used in our previous example (codegen.yml):
1schema: http://localhost:3000/graphql
2documents: ./src/graphql/*.graphql
3generates:
4  graphql/generated.ts:
5    plugins:
6      - typescript-operations
7      - typescript-react-apollo
8config:
9   withHooks: false
10
This configuration will generate a src/graphql/generated.ts file that will contain:
  • GraphQL document nodes
  • TypeScript Query/Mutation Result types (return type of our GraphQL operations)
  • TypeScript Query/Mutation Variables types (variables types of our GraphQL operations)
Here an example of GraphQL Code Generator output (src/graphql/generated.ts) given our previous currentUser Query:
1import { gql } from '@apollo/client';
2import * as Apollo from '@apollo/client';
3export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
4export type CurrentUserQuery = (
5  { __typename?: 'Query' }
6  & { me: (
7    { __typename?: 'User' }
8    & Pick<User, 'id'>
9  ) }
10);
11
12export const CurrentUserDocument = gql`
13    query currentUser {
14  me {
15    id
16  }
17}
18`;
19
20export type CurrentUserQueryResult = Apollo.QueryResult<CurrentUserQuery, CurrentUserQueryVariables>;
21
We already saw the benefits of these generated types on the <Login> component refactoring.
However, we can agree that having to provide both the query TypeScript type (CurrentUserQueryResult) and the query GraphQL document node (currentUserDocument) to useQuery() is cumbersome: useQuery<CurrentUserQueryResult>(currentUserDocument).
Let’s see how we can improve that in the next configuration mode.

Generate Typed React Hooks

GraphQL Code Generator is capable of more than just generating TypeScript types, it can also generate JavaScript/TypeScript code. Let’s see how we can ask to GraphQL Code Generator to generate Typed React hooks, so we don’t have to provide the TypeScript types to useQuery() every time.
Let’s use the following configuration:
1schema: http://localhost:3000/graphql
2documents: ./src/graphql/*.graphql
3generates:
4  graphql/generated.ts:
5    plugins:
6      - typescript-operations
7      - typescript-react-apollo
8
This configuration will generate a src/graphql/generated.ts file that will contain:
  • GraphQL document node
  • TypeScript Query/Mutation Result types (return type of our GraphQL operations)
  • TypeScript Query/Mutation Variables types (variables types of our GraphQL operations)
  • One custom hook for each defined GraphQL operation
Example given our previous currentUser Query (src/graphql/generated.ts):
1import { gql } from '@apollo/client';
2import * as Apollo from '@apollo/client';
3const defaultOptions =  {}
4export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
5export type CurrentUserQuery = (
6  { __typename?: 'Query' }
7  & { me: (
8    { __typename?: 'User' }
9    & Pick<User, 'id'>
10  ) }
11);
12
13export const CurrentUserDocument = gql`
14    query currentUser {
15  me {
16    id
17  }
18}
19`;
20
21export function useCurrentUserQuery(baseOptions?: Apollo.QueryHookOptions<CurrentUserQuery, CurrentUserQueryVariables>) {
22        const options = {...defaultOptions, ...baseOptions}
23        return Apollo.useQuery<CurrentUserQuery, CurrentUserQueryVariables>(CurrentUserDocument, options);
24}
25export type CurrentUserQueryHookResult = ReturnType<typeof useCurrentUserQuery>;
26export type CurrentUserQueryResult = Apollo.QueryResult<CurrentUserQuery, CurrentUserQueryVariables>;
27
Which will give us this updated version of our <Login> component:
1import { useCurrentUserQuery  } from "../graphql/generated.ts"
2
3// no need to create the User type or `gql` query, we import them from the generated file
4
5const Login = () => {
6   const { data } = useCurrentUserQuery()
7   // user is typed!
8   const user = data ? data.me : null
9
10   // ...
11}
12
Nice! Isn’t it?

Generate Typed Documents

GraphQL Code Generator is providing another simple way to use typed GraphQL Query and Mutations, called TypedDocumentNode.
With the following configuration:
1schema: http://localhost:3000/graphql
2documents: ./src/graphql/*.graphql
3generates:
4  graphql/generated.ts:
5    plugins:
6      - typescript-operations
7      - typed-document-node
8
GraphQL Code Generator will generate the following file (src/graphql/generated.ts):
1import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
2export type CurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
3export type CurrentUserQuery = (
4  { __typename?: 'Query' }
5  & { me: (
6    { __typename?: 'User' }
7    & Pick<User, 'id'>
8  ) }
9);
10
11export const CurrentUserDocument: DocumentNode<CurrentUserQuery, CurrentUserQueryVariables> = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"currentUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"me"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]};
12
This allows us the following refactoring of our component:
1import { CurrentUserDocument  } from "../graphql/generated.ts"
2
3// no need to create the User type or `gql` query, we import them from the generated file
4
5const Login = () => {
6   const { data } = useQuery(CurrentUserDocument)
7   // user is typed!
8   const user = data ? data.me : null
9
10   // ...
11}
12
The TypedDocumentNode approach is more scalable than the hooks generation. The generation of one custom hook per GraphQL operation (Query/Mutation) can generate a LOT of hooks at scale along with a lot of imports, which is not necessary given the useMutation() useQuery provided by Apollo Client.

Tips: Leverage GraphQL Fragments for scalable types

Now that we have many ways to generate **stable **data types, let’s see how to make them easier to use and maintain in time.
Let’s take a look at the following helper:
1import { CurrentUserQuery } from "src/graphql/generated";
2
3const isUserEmailValid = (user: CurrentUserQuery["me']) => !!user.email
4
Here, instead of using our currentUser query CurrentUserQuery[“me”] type, we would prefer to rely on a User type.
We can achieve this with zero maintainability by leveraging GraphQL Fragments.
When Fragments are provided, GQL Code Generator will produce the corresponding TypeScript types. Here is our updated src/graphql/queries.graphql:
1query currentUser {
2   me {
3      ...User
4   }
5}
6
The ...User indicates to GraphQL that we want to expand our User fragment here, similar to the object spread syntax.
In order to do so, we need to provide to GraphQL Code Generator the definition of the User fragment that we will place in a new src/graphql/fragments.graphql file:
1fragment User on users {
2   id
3   email
4} 
5
Please note that a fragment needs to be defined against an existing type of the GraphQL API Schema, here users.
Here is our updated helper code:
1import { UserFragment } from 'src/graphql/generated';
2const isUserEmailValid = (user: UserFragment) => !!user.email
3
Leveraging GraphQL Fragments allows you to build your React app data types on top of the GraphQL API types.
Please note that multiple fragments can be defined on a single GraphQL Schema type:
1fragment User on users {
2   id
3   email
4}
5 
6fragment UserProfile on users {
7   id
8   email
9   firstName
10   lastName
11} 
12
A good practice is to ensure that all your Query and Mutations responses use fragment, this will ensure that your React application can benefit from well-defined data types of different specificity, ex:
  • User type carries the necessary base properties
  • UserProfile type carries the minimum user info for display
  • UserExtended type carries all the users properties

Up next

Now that our strong data types are in-sync with the API, let's an a look at how to deal with untyped code.
We use cookies to collect statistics through Google Analytics.
Do not track
 
Allow cookies