TypeScript graphql

How To Generate TypeScript Types From GraphQL

Posted on September 30th, 2022 by Antonello Zanini

Using types in your applications makes your code more secure, consistent, and robust. Types also allow you to detect and avoid mistakes as early as possible in the development process. This is particularly true for GraphQL types, which play a key role in your frontend application and enable you to avoid unexpected bugs and errors.

Writing types manually is time-consuming, cumbersome, and error-prone. Luckily, you can use a code generator to automatically generate TypeScript types from GraphQL queries.
Here, you will learn why adopting GraphQL types is so important, how to use graphql-codegen to generate TypeScript types from your .graphql files, why nullable fields may represent a problem, and how you can tackle this. 

Why You Should Generate GraphQL Typescript Types

By default, GraphQL validates all requests and returns only schema-valid responses. However, your frontend application may not know what a schema-valid response looks like. This is especially true when using untyped languages, such as JavaScript. In this case, you might accidentally access nonexistent properties or forget to implement the right checks on null. This could lead to several errors in the application.

By using a typed language and adopting GraphQL types, you can avoid all this. To define types, you should use a GraphQL code generator. This is because it enables you to automatically generate the required TypeScript types from your GraphQL queries. Specifically, GraphQL code generators generate input and output types for GraphQL requests in your code. This means that by generating GraphQL TypeScript types, you can achieve extra validation for your GraphQL API calls. Also, accessing and using the results of your GraphQL requests will become easier and safer.

Generating Typescript Types From GraphQL With graphql-codegen

Let’s now learn how to automatically generate TypeScript types from your GraphQL queries with graphql-codegen.

Here, you will see an example based on a demo application that uses Next.js as the frontend technology and DatoCMS as the backend technology. Keep in mind that any TypeScript frontend technology and GraphQL-based backend technology will do.

Clone the GitHub repository supporting the article and run the demo application with the following commands:

Terminal window
git clone https://github.com/datocms/typescript-type-generation-graphql-example
cd typescript-type-generation-graphql-example
npm i
npm run dev

Now, follow this step-by-step tutorial and learn how to generate TypeScript GraphQL types with graphql-codegen.

Prerequisites

If you are a DatoCMS user, initialize a new project and create an Article model as follows:

First, you will need the following libraries:

You can install them with:

Terminal window
npm i graphql graphql-request


graphql-request is a lightweight and minimal library developed Prisma Labs that provides exactly what you need to perform GraphQL requests. Note that any other GraphQL client, such as Apollo GraphQL, will do.

Then, you will need the following libraries to make the code generation work:


You can add all these libraries to your project’s dependencies with the following npm command:

Terminal window
npm i @graphql-codegen/cli @graphql-codegen/typed-document-node @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-typed-document-node/core


@graphql-typed-document-node/core is required to implement the TypedDocumentNode approach. Learn more here

The first four libraries belong to the GraphQL codegen project. If you are not familiar with this project, GraphQL codegen gives you the ability to generate code from your GraphQL schema and operations
This is all you need to generate TypeScript types from your GraphQL queries.

Let’s now see how to do it! 

Setting Up graphql-codegen

To make graphql-codegen work, you need to define a GraphQL configuration file.

Create graphql.config.yml file in the root directory of your project and initialize it as follows:

schema:
- https://graphql.datocms.com:
headers:
Authorization: "Bearer <YOUR_DATOCMS_API_TOKEN>"
documents: './graphql/**/*.graphql'
generates:
graphql/generated.ts:
plugins:
- typescript
- typescript-operations
- typed-document-node
config:
strictScalars: true
scalars:
BooleanType: boolean
CustomData: Record<string, unknown>
Date: string
DateTime: string
FloatType: number
IntType: number
ItemId: string
JsonField: unknown
MetaTagAttributes: Record<string, string>
UploadId: string
# Optional, gives meta fields a leading underscore
namingConvention:
# enumValues: './pascalCaseWithUnderscores'

Note that the documents field must contain the path to the .graphql files storing your GraphQL queries. Also, change the schema field with the URL to your GraphQL schema. In this case, the GraphQL schema is retrieved directly from DatoCMS. Learn more on how to use GraphQL with DatoCMS.

If you are wondering what the scalars mentioned in the graphql.config.yml file are, take a look at this.

In a nutshell, DatoCMS uses some custom GraphQL scalar types. Therefore, you need to define them in your config file in order for the code generator to be able to transform the results retrieved from the DatoCMS Content Delivery API into TypeScript types.

Keep also in mind that the graphql.config.yml file can be used in the GraphQL extension for Visual Studio Code to get auto-completion and error checking directly in the IDE on GraphQL queries.

You are now ready to generate TypeScript types with graphql-codegen.

Optional: Leading underscores for meta fields

Optionally, you can use create the file ./pascalCaseWithUnderscores.js to give your generated enums for meta fields a leading underscore, like _CreatedAtAsc instead of CreatedAtAsc. That file should look like this:

/*
This the optional file /pascalCaseWithUnderscores.js
If you add this file to `namingConvention.enumValues` in your
`graphql.config.yml` file, it will add leading underscores where appropriate
*/
const { pascalCase } = require("change-case-all")
function pascalCaseWithUnderscores(str) {
const result = pascalCase(str)
if (!result) {
return str
}
// if there is a leading underscore but it's not in the converted string, add it
if (str.indexOf("_") === 0 && result.substring(0, 1) !== "_") {
return `_${result}`
}
return result
}
module.exports = pascalCaseWithUnderscores

Generating TypeScript Types With graphql-codegen

First, add the following line to the scripts section of your package.json file:

"generate-ts-types": "graphql-codegen --config graphql.config.yml",

This will launch the graphql-codegen type generation command as defined in the graphql.config.yml configuration file defined above. You can learn more about the arguments supported by this command here.

Now, launch the command below:

Terminal window
npm run generate-ts-types


Wait for the code generation process to end, and you should get:

Terminal window
Parse Configuration
Generate outputs

Now, enter the ./graphql directory, and you should be able to see a new generated.ts file. This contains all the TypeScript types generated by graphql-codegen.

In this simple DatoCMS example with only the Article model, generated.ts will look like as below:

import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core"
export type Maybe<T> = T | null
export type InputMaybe<T> = Maybe<T>
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> }
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> }
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string
String: string
Boolean: boolean
Int: number
Float: number
BooleanType: boolean
CustomData: Record<string, unknown>
DateTime: string
FloatType: number
IntType: number
ItemId: string
JsonField: unknown
MetaTagAttributes: Record<string, string>
UploadId: string
}
export type ArticleModelContentField = {
__typename?: "ArticleModelContentField"
blocks: Array<Scalars["String"]>
links: Array<Scalars["String"]>
value: Scalars["JsonField"]
}
export type ArticleModelFilter = {
AND?: InputMaybe<Array<InputMaybe<ArticleModelFilter>>>
OR?: InputMaybe<Array<InputMaybe<ArticleModelFilter>>>
_createdAt?: InputMaybe<CreatedAtFilter>
_firstPublishedAt?: InputMaybe<PublishedAtFilter>
_isValid?: InputMaybe<BooleanFilter>
_publicationScheduledAt?: InputMaybe<PublishedAtFilter>
_publishedAt?: InputMaybe<PublishedAtFilter>
_status?: InputMaybe<StatusFilter>
_unpublishingScheduledAt?: InputMaybe<PublishedAtFilter>
_updatedAt?: InputMaybe<UpdatedAtFilter>
content?: InputMaybe<StructuredTextFilter>
id?: InputMaybe<ItemIdFilter>
image?: InputMaybe<FileFilter>
slug?: InputMaybe<SlugFilter>
title?: InputMaybe<StringFilter>
}
// Truncated for brevity...

You can have a look at the entire file here.

Now, let’s learn how to use these TypeScript types to perform GraphQL queries. 

How To Use GraphQL API With Generated TypeScript Types

First, define a request.ts file in your lib folder as follows:

import { request as graphqlRequest, Variables } from "graphql-request"
import { RequestDocument } from "graphql-request/dist/types"
import { TypedDocumentNode } from "@graphql-typed-document-node/core"
export function request<TDocument = any>(
document: RequestDocument | TypedDocumentNode<TDocument, Variables>,
variables?: Variables,
) {
return graphqlRequest<TDocument, Variables>("https://graphql.datocms.com/", document, variables, {
Authorization: "xxx",
})
}

Here, the graphql-request client library is used to define a request() function. This performs GraphQL requests based on the TypedDocumentNode approach and returns typed results.

Let’s now see request() in action:

import { GetStaticProps, NextPage } from "next"
import { Image as DatoImage } from "react-datocms"
import { HomeDocument, HomeQuery } from "../graphql/generated"
import { request } from "../lib/request"
const Home: NextPage<Props> = ({ result }) => {
return (
<ul>
{result.allArticles.map((article) => (
<li key={article.id}>
{article.title} {article.image?.responsiveImage && <DatoImage data={article.image.responsiveImage} />}
</li>
))}
</ul>
)
}
type Props = { result: HomeQuery }
export const getStaticProps: GetStaticProps<Props> = async (context) => {
// retrieving the list of all articles
const result = await request(HomeDocument)
return {
props: { result },
}
}
export default Home

As you can see, you can perform a GraphQL request simply by using the TypeScript auto-generated types and objects. All you have to do is pass the right TypedDocumentNode to request() and it will return the expected typed result. 

In detail, note that the result object returned by request() contains the allArticles field storing the list of Article objects retrieved by the GraphQL query defined in the ./graphql/home.graphql file below:

query Home {
allArticles {
id
title
_createdAt
_publishedAt
image {
...responsiveImage
}
}
}
fragment responsiveImage on FileFieldInterface {
responsiveImage(imgixParams: { w: 100, h: 100, fit: crop }) {
alt
aspectRatio
base64
bgColor
height
sizes
src
srcSet
title
webpSrcSet
width
}
}

If you are wondering what HomeDocument and HomeQuery look like, you can find their definition in your generated.ts file.

Also, note that the Home component uses the DatoCMS Next.js Image component. Learn more about what DatoCMS has to offer when it comes to handling images in Next.js.

Congrats! You just learned how to automatically generate TypeScript types and objects from your GraphQL queries and use them to effortlessly perform GraphQL requests!

Why Required Fields Are Nullable When Using a Headless CMS

Headless CMS solutions usually allow you to define data models and populate entities accordingly. A data model is composed of several fields, each of which can be marked as required. When a field is considered required, it should always have a value. However, read carefully the following example and consider having a look at this thread.

Let’s suppose you have an Article model defined as seen earlier. You have already created several article records, and you want to extend the Article model with a new subtitle required field.

TypeScript GraphQL
DatoCMS field

Now, every time you create an article record, you will have to specify a subtitle. On the other hand, the subtitle field will be initialized as null on already existing records, despite the required validation. 

Therefore, any required field may actually have a null value. For this reason, all required fields are nullable in the GraphQL schema. Keep in mind that this is not a DatoCMS behavior, but an approach followed by all major headless CMS solutions.

Consequently, when automatically generating the TypeScript types from your GraphQL schema, all required fields will be marked as nullable

export type ArticleRecord = RecordInterface & {
__typename?: 'ArticleRecord';
_createdAt: Scalars['DateTime'];
/** Editing URL */
_editingUrl?: Maybe<Scalars['String']>;
_firstPublishedAt?: Maybe<Scalars['DateTime']>;
_isValid: Scalars['BooleanType'];
_modelApiKey: Scalars['String'];
_publicationScheduledAt?: Maybe<Scalars['DateTime']>;
_publishedAt?: Maybe<Scalars['DateTime']>;
/** Generates SEO and Social card meta tags to be used in your frontend */
_seoMetaTags: Array<Tag>;
_status: ItemStatus;
_unpublishingScheduledAt?: Maybe<Scalars['DateTime']>;
_updatedAt: Scalars['DateTime'];
content?: Maybe<ArticleModelContentField>;
id: Scalars['ItemId'];
image?: Maybe<FileField>;
slug?: Maybe<Scalars['String']>;
subtitle?: Maybe<Scalars['String']>;
title?: Maybe<Scalars['String']>;
};

This is the auto-generated ArticleRecord TypeScript type. Notice that slug, subtitle, and title are all nullable, even if they are all marked as required.

This means that you always have to use the optional chaining operator, even if subtitle is required.

// subtitle may be null despite the required validation
article.subtitle?.toUpperCase()

Keep in mind that you have to use the optional chaining operator or define null checks on subtitle on all record entities, even though it may not be null on any record.

Note that DatoCMS would also mark article records with a null value for required fields as invalid through a false value on the is_valid attribute. 

Filling your code with potentially unnecessary null checks is not elegant and makes your code dirtier, harder to read and maintain.

Fortunately, DatoCMS has a solution!

How To Avoid Nullable Types on Required Field in DatoCMS

DatoCMS allows you to globally exclude invalid records without always specifying an _isValid filter in your queries. You can achieve this by setting the following header:

X-Exclude-Invalid: true

This enables the so-called strict-mode for non-nullable GraphQL types. In detail, when this mode is enabled, any field marked as required will be associated with a non-nullable GraphQL type. Learn more about the DatoCMS Strict Mode for GraphQL.

Now, let’s see how to use this special header.

Update your graphql.config.yml file as follows:

schema:
- https://graphql.datocms.com:
headers:
Authorization: "<YOUR_DATOCMS_API_TOKEN>"
X-Exclude-Invalid: true
# omitted for brevity...


And your request.ts file as below:

import { request as graphqlRequest, Variables } from "graphql-request"
import { RequestDocument } from "graphql-request/dist/types"
import { TypedDocumentNode } from "@graphql-typed-document-node/core"
export function request<TDocument = any, TVariables = Record<string, any>>(
document: RequestDocument | TypedDocumentNode<TDocument, Variables>,
variables?: Variables,
) {
return graphqlRequest<TDocument, Variables>("https://graphql.datocms.com/", document, variables, {
Authorization: "<YOUR_DATOCMS_API_TOKEN>",
"X-Exclude-Invalid": "true",
})
}


Now, relaunch the code generation command:

Terminal window
npm run generate-ts-types


And generated.ts will contain the following type:

export type ArticleRecord = RecordInterface & {
__typename?: "ArticleRecord"
_createdAt: Scalars["DateTime"]
/** Editing URL */
_editingUrl?: Maybe<Scalars["String"]>
_firstPublishedAt?: Maybe<Scalars["DateTime"]>
_isValid: Scalars["BooleanType"]
_modelApiKey: Scalars["String"]
_publicationScheduledAt?: Maybe<Scalars["DateTime"]>
_publishedAt?: Maybe<Scalars["DateTime"]>
/** Generates SEO and Social card meta tags to be used in your frontend */
_seoMetaTags: Array<Tag>
_status: ItemStatus
_unpublishingScheduledAt?: Maybe<Scalars["DateTime"]>
_updatedAt: Scalars["DateTime"]
content: ArticleModelContentField
id: Scalars["ItemId"]
image?: Maybe<FileField>
slug: Scalars["String"]
subtitle: Scalars["String"]
title: Scalars["String"]
}

Now, slug, subtitle, and title in the ArticleRecord type are non-nullable as expected. 

Et voilà! You no longer have to use unnecessary null checks in your TypeScript codebase.

Conclusion

In this article, you learned why you need GraphQL TypeScript types and how to automatically generate them from your GraphQL queries. This can be easily achieved with a GraphQL code generator. In detail,  here you learned how to generate TypeScript types from GraphQL with graphql-codegen.

However, keep in mind that code generation comes with some challenges. This is particularly true when the GraphQL schema is defined through a headless CMS. In fact, required fields are generally marked as null.

Here, you learned how you can avoid this with DatoCMS, the powerful, fully-featured, easy-to-use headless CMS for developers and marketers.

Thanks for reading! We hope that you found this article helpful. Feel free to reach out to us on Twitter with any questions, comments, or suggestions.

Start using DatoCMS today
According to Gartner 89% of companies plan to compete primarily on the basis of customer experience this year. Don't get caught unprepared.
  • No credit card
  • Easy setup
Subscribe to our newsletter! 📥
One update per month. All the latest news and sneak peeks directly in your inbox.
support@datocms.com ©2024 Dato srl, all rights reserved P.IVA 06969620480