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:
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:
graphql >= 16.6.0
graphql-request >= 5.0.0
You can install them with:
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:
@graphql-codegen/cli >= 2.12.0
@graphql-codegen/typed-document-node >= 2.3.3
@graphql-codegen/typescript >= 2.7.3
@graphql-typed-document-node/core >= 3.1.1
You can add all these libraries to your project’s dependencies with the following npm command:
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:
npm run generate-ts-types
Wait for the code generation process to end, and you should get:
✔ 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 | nullexport 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.
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 validationarticle.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:
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.