🌐

GraphQLのスキーマ定義やクエリから型定義、自動生成できまっせ

2022/06/26に公開

概要

  • 2022年のフロントエンドに必要なスキルマップにもあるように、GraphQLという選択肢が一般化しています。

スクリーンショット 2022-06-26 10.33.09.png
引用元

  • GraphQLを使用する場合、GraphQLの特性を知らなければなりませんが、今回の記事ではGraphQLとは何か。どう使うのかについては言及致しません。その場合は、こちらの記事を一読していただけますと幸いです。

https://zenn.dev/yoshii0110/articles/2233e32d276551

  • 今回は、GraphQLを使用する際にGraphQLのスキーマやクエリからTypeScriptの型定義を自動生成する方法について記事にしていきます。

GraphQLのスキーマやクエリからTypeScriptの型定義を自動生成するには

  • 例えば、フロントエンド開発においてTypeScriptの導入をしている場合、GraphQLのスキーマごとの型定義を作成しなければなりません。
  • ただ、GraphQLのスキーマを確認しながら型を手動で作っていくのってしんどいですよね。。
  • そんな時に使用できるのが、GraphQL Code Generatorです。

GraphQL Code Generator

  • GraphQL Code Generatorというのは、GraphQLのスキーマ定義を使用して型定義を自動生成するためのツールです。
  • このスキーマ定義の型定義を作成することで、クライアントサイドからのGraphQLのリクエストとレスポンスに型をつけることができます。
  • さまざまなフレームワーク、ライブラリに対応しているので、用途にあったプラグインを組み合わせて使用します。
  • また、フロントエンドでGraphQLを使用する場合とバックエンドでGraphQLを使用する場合、それぞれプラグインや実装方法が異なるので、注意が必要です。
  • 今回はパターンを絞りフロントエンド側でGraphQLを使用するパターンを試していきます。

クライアント側の型定義生成

フロントエンド側でGraphQLを使用する場合

  • まず、GraphQL Code Generatorの便利さを伝えるために、GraphQL Code Generatorを使用しない場合について簡単に説明します。
  • 下記のようなGraphQLのschemaがあったとします。
type Author {
  id: Int!
  firstName: String!
  lastName: String!
  posts(findTitle: String): [Post]
}

type Post {
  id: Int!
  title: String!
  author: Author
}

type Query {
  posts: [Post]
}
  • フロントエンドでGraphQLを使用する場合は上記のスキーマ定義を確認しつつ、クライアント側の実装は、次のようにしAPIをリクエストします。
  • なお下記のsampleはフロントエンド側をReactで実装した場合のものになります。
import { request, gql } from 'graphql-request'
import { useQuery } from 'react-query'

interface PostQuery {
  posts: {
    id: string
    title: string
    author?: {
      id: string
      firstName: string
      lastName: string
    }
  }[]
}

const postsQueryDocument = gql`
  query Posts {
    posts {
      id
      title
      author {
        id
        firstName
        lastName
      }
    }
  }
`

const Posts = () => {
  const { data } = useQuery<PostQuery>('posts', async () => {
    const { posts } = await request(endpoint, postsQueryDocument)
    return posts
  })

  // ...
}
  • GraphQLのリクエストをする際にPostQueryのinterfaceを作成し、それを使用していることがわかりますね。
  • この手動管理、何が問題かというと例えば、スキーマ定義がバックエンド側で更新された場合、古い型を更新しないといけません。
  • あとは、スキーマの定義を確認しながら手動で型を作成しているので型定義のミスが発生する可能性もあります。
  • そして何より手動で型定義作るのはしんどいです。。

ReactでGraphQLを使用している場合のプラグインごとのパターン

  • Reactを使用し、GraphQLエンドポイントに対してAPIリクエストを投げているプロジェクトの場合、GraphQLの使用方法がいくつかあります。
  • GraphQLとTypeScriptでReactQueryを使用するパターン
  • Apollo Clientを使用するパターン

上記の2つについてそれぞれ見ていきましょう。

GraphQLとTypeScriptでReactQueryを使用するパターン

  • 下記にTypeScriptでReactQueryを使用する場合、今までは、下記のように型定義を手動で作成しGraphQLエンドポイントにAPIリクエストを送信します。
import { useQuery } from 'react-query'
import { request, gql } from 'graphql-request'

interface PostQuery {
  posts: {
    id: string
    title: string
    author?: {
      id: string
      firstName: string
      lastName: string
    }
  }[]
}

const postsQueryDocument = gql`
  query Posts {
    posts {
      id
      title
      author {
        id
        firstName
        lastName
      }
    }
  }
`

const Posts = () => {
  const { data } = useQuery<PostQuery>('posts', async () => {
    const { posts } = await request(endpoint, postsQueryDocument)
    return posts
  })

  // ...
}
  • この場合は、@graphql-codegen/typescript-react-queryプラグインを使用します。

①: npmやyarnでプラグインのインストールを行いましょう。

npmの場合

npm install @graphql-codegen/typescript-react-query
npm install @graphql-codegen/typescript
npm install @graphql-codegen/typescript-operations

yarnの場合

yarn add @graphql-codegen/typescript-react-query
yarn add @graphql-codegen/typescript
yarn add @graphql-codegen/typescript-operations

②: 次にプラグインのConfigureを作成していきます。

  • codegen.yamlというファイルを作成し、schemaには、使用しているGraphqlのスキーマ情報のパスを指定します。
schema: http://my-graphql-api.com/graphql
documents: './src/**/*.tsx'
generates:
  ./graphql/generated.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-query
    config:
      fetcher: fetch

③: codegenを実行し、コードを更新するnpm scriptを記載します。

  • package.jsonに下記のようなコマンドを追加しましょう。
{
  "scripts": {
    "generate": "graphql-codegen"
  }
}

④: 最後に下記のコマンドを実行し、graphql/generated.tsx(スキーマの型定義ファイル)を自動生成します。

npmの場合

npm run generate

yarnの場合

yarn generate

⑤: 最後にGraphQLのエンドポイントにAPIのリクエストを送信している箇所を下記のように変更します。

  • 簡単に言えば、上記で自動生成された型定義ファイルをimportし使用するように書き換える形です。
import gql from 'graphql-tag'
import { useQuery } from 'react-query'
import { usePosts } from '../graphql/generated'

gql`
  query Posts {
    posts {
      id
      title
      author {
        id
        firstName
        lastName
      }
    }
  }
`

const Posts = () => {
  const { data } = usePosts()

  // `data` is typed!

  // ...
}

Apollo Clientを使用するパターン

  • Apollo Clientを使用している場合は、今までは下記のように型定義を手動で作成しGraphQLエンドポイントにAPIリクエストを送信します。
import { gql, useQuery } from '@apollo/client'

interface PostQuery {
  posts: {
    id: string
    title: string
    author?: {
      id: string
      firstName: string
      lastName: string
    }
  }[]
}

const postsQueryDocument = gql`
  query Posts {
    posts {
      id
      title
      author {
        id
        firstName
        lastName
      }
    }
  }
`

const Posts = () => {
  const { data } = useQuery<PostQuery>(postsQueryDocument)

  // ...
}

こちらも、自動生成された型定義ファイルを使用すると下記のようにシンプルになります。

import { useQuery } from '@apollo/client'
import { postsQueryDocument } from './graphql/generated'

const Posts = () => {
  const { data } = useQuery(postsQueryDocument)

  // `result` is fully typed!
  // ...
}

①: npmやyarnでプラグインのインストールを行いましょう。

npmの場合

npm install @graphql-codegen/typed-document-node
npm install @graphql-codegen/typescript
npm install @graphql-codegen/typescript-operations

yarnの場合

yarn add @graphql-codegen/typed-document-node
yarn add @graphql-codegen/typescript
yarn add @graphql-codegen/typescript-operations

②: 次にプラグインのConfigureを作成していきます。

  • codegen.yamlというファイルを作成し、schemaには、使用しているGraphqlのスキーマ情報のパスを指定します。
schema: http://my-graphql-api.com/graphql
documents: './src/**/*.graphql'
generates:
  ./src/generated.ts:
    plugins:
      - typescript
      - typescript-operations
      - typed-document-node

③: codegenを実行し、コードを更新するnpm scriptを記載します。

  • package.jsonに下記のようなコマンドを追加しましょう。
{
  "scripts": {
    "generate": "graphql-codegen"
  }
}

④: 最後に下記のコマンドを実行し、graphql/generated.tsx(スキーマの型定義ファイル)を自動生成します。

npmの場合

npm run generate

yarnの場合

yarn generate

実際にやってみた

  • そしたら実際に型定義が自動生成されるのか試してみましょう。
  • 先ほど説明したプラグインのパターン以外にもtypescript-graphql-requestというライブラリを使用したパターンなんかもあるので、今回はそちらを試してみましょう。

cliのインストール

  • 下記のコマンドでCLIのインストールを行います。
  • また、graphql-codegenのCLIを使用する場合、graphqlもインストールする必要があります。

npmの場合

npm install graphql
npm install @graphql-codegen/cli

yarnの場合

yarn add graphql
yarn add @graphql-codegen/cli
  • インストールすると、graphql-codegen/cliのセットアップをする必要があります。

npmの場合

npx graphql-codegen init
npm run install

今回使用するプラグイン

  • 今回は、以下のプラグインを使用していきます。
npm install @graphql-codegen/typescript
npm install @graphql-codegen/typescript-operations
npm install @graphql-codegen/typescript-graphql-request
  • プラグインのConfigureは下記のようにします。
schema: "https://raw.githubusercontent.com/marmelab/GraphQL-example/master/schema.graphql"
documents: "query.graphql"
generates:
  src/generated/graphql.tsx:
    plugins:
      - "typescript"
      - "typescript-operations"
  • 今回使用するサンプルのスキーマは、下記のものです。

https://raw.githubusercontent.com/marmelab/GraphQL-example/master/schema.graphql

  • 上記のスキーマをもとにリクエストを送るクエリを作成しましょう。

スクリーンショット 2022-06-26 12.05.08.png

query TweetById ($id: ID!) {
  Tweet(id: $id) {
    id
    body
    date
    Author {
        id
        username
    }
  }
}
  • ここまでできればあとは、自動生成するコマンドを実行するだけです。
npm run generate

> my-app@0.1.0 generate
> graphql-codegen --config codegen.yml

(node:26606) ExperimentalWarning: The Fetch API is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
  ✔ Parse configuration
  ✔ Generate outputs
  • 自動生成することができました。
  • 最後に自動生成されたファイルの中身を確認してみましょう。

スクリーンショット 2022-06-26 12.15.12.png

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;
  Date: any;
  Url: any;
};

export type Meta = {
  __typename?: 'Meta';
  count?: Maybe<Scalars['Int']>;
};

export type Mutation = {
  __typename?: 'Mutation';
  createTweet?: Maybe<Tweet>;
  deleteTweet?: Maybe<Tweet>;
  markTweetRead?: Maybe<Scalars['Boolean']>;
};


export type MutationCreateTweetArgs = {
  body?: InputMaybe<Scalars['String']>;
};


export type MutationDeleteTweetArgs = {
  id: Scalars['ID'];
};


export type MutationMarkTweetReadArgs = {
  id: Scalars['ID'];
};

export type Notification = {
  __typename?: 'Notification';
  date?: Maybe<Scalars['Date']>;
  id?: Maybe<Scalars['ID']>;
  type?: Maybe<Scalars['String']>;
};

export type Query = {
  __typename?: 'Query';
  Notifications?: Maybe<Array<Maybe<Notification>>>;
  NotificationsMeta?: Maybe<Meta>;
  Tweet?: Maybe<Tweet>;
  Tweets?: Maybe<Array<Maybe<Tweet>>>;
  TweetsMeta?: Maybe<Meta>;
  User?: Maybe<User>;
};


export type QueryNotificationsArgs = {
  limit?: InputMaybe<Scalars['Int']>;
};


export type QueryTweetArgs = {
  id: Scalars['ID'];
};


export type QueryTweetsArgs = {
  limit?: InputMaybe<Scalars['Int']>;
  skip?: InputMaybe<Scalars['Int']>;
  sort_field?: InputMaybe<Scalars['String']>;
  sort_order?: InputMaybe<Scalars['String']>;
};


export type QueryUserArgs = {
  id: Scalars['ID'];
};

export type Stat = {
  __typename?: 'Stat';
  likes?: Maybe<Scalars['Int']>;
  responses?: Maybe<Scalars['Int']>;
  retweets?: Maybe<Scalars['Int']>;
  views?: Maybe<Scalars['Int']>;
};

export type Tweet = {
  __typename?: 'Tweet';
  Author?: Maybe<User>;
  Stats?: Maybe<Stat>;
  body?: Maybe<Scalars['String']>;
  date?: Maybe<Scalars['Date']>;
  id: Scalars['ID'];
};

export type User = {
  __typename?: 'User';
  avatar_url?: Maybe<Scalars['Url']>;
  first_name?: Maybe<Scalars['String']>;
  full_name?: Maybe<Scalars['String']>;
  id: Scalars['ID'];
  last_name?: Maybe<Scalars['String']>;
  /** @deprecated Field no longer supported */
  name?: Maybe<Scalars['String']>;
  username?: Maybe<Scalars['String']>;
};

export type TweetByIdQueryVariables = Exact<{
  id: Scalars['ID'];
}>;


export type TweetByIdQuery = { __typename?: 'Query', Tweet?: { __typename?: 'Tweet', id: string, body?: string | null, date?: any | null, Author?: { __typename?: 'User', id: string, username?: string | null } | null } | null };

めちゃめちゃいい感じですね!

他の選択肢

  • ちなみにGraphQL Code Generator以外にも下記ライブラリもあります。
  • こちらはいずれ別記事で紹介できればと思います。

https://github.com/tgriesser/apollo-codegen

まとめ

  • 今まで、GraphQLのスキーマ定義からAPIのリクエストに使用する型定義を手動で作っていたのですが、こんな便利なツールがあったとは、目から鱗です。
  • 是非お試しください!

参考

https://www.graphql-code-generator.com/docs/getting-started

https://techlife.cookpad.com/entry/2021/03/24/123214

https://zenn.dev/sky/articles/47b86d3387389d

Discussion