Galapagos Tech Blog

株式会社ガラパゴスのメンバーによる技術ブログです。

graphql-codegenで簡単3分!TypeScript型定義。

こんにちわ。
私の記憶もブロックチェーンにしてしまいたいと思う今日この頃。
(報酬は私の特製おにぎりで勘弁)
AIR Design for Apps事業部、社内システムチームの高橋です。

私の事業部では案件管理を
独自の社内システムを開発し運用しています。

その社内システムのフロントエンドではフレームワークにReact
GraphQLクライアントにApollo Client
開発言語はTypeScriptという構成で開発を行なっています。
(バックエンドではWeb-APIとしてGraphQL)

システムが本番リリースしてからフロントエンド開発に携わったため
普段、開発に用いていたライブラリの利便性について
あまり考えることはありませんでした。
が、改めて調べてみるとこりゃ良いなと思った
graphql-codegen というライブラリについて今回はご紹介したいとます。

graphql-codegenとは

公式リポジトリには下記の説明があります。

GraphQL Code Generatorは、GraphQLスキーマからコードを生成するツールです。

参照 github.com
フロントエンド側でGraphQLを呼び出す時に
クエリに渡すパラメータやモデル、レスポンス内容といった
TypeScript型定義をGraphQLスキーマから定義してくれます。

バックエンド側のスキーマに変更があった場合も
コマンド一つで既存の型定義を更新してくれます。

初期設定を変更すればApollo Clientのカスタムフックも定義してくれて超便利。
いわゆる開発を効率化してれるっていうツールです。

GraphQLスキーマとは

簡単に言うとGraphQLの型です。

GraphQLの詳細は今回は割愛しますが
GraphQLではカスタムスカラー型からモデル型
各クエリ型の定義まで様々なものを型定義します。

この明確な定義によって単一のエンドポイントから特定のクエリを呼び出したり
パラメータや各フィールドのバリデーションをGraphQLサーバー側で行うことが出来ます。
またGraphQLの特徴にもつながる大事な構成要素となっています。

使ってみた

どんなツールかざっくりと分かったところで
使用しないとどんな実装になって、使用するとどんな実装になってメリットを享受できるか
具体的にApollo Client公式ドキュメントより
コードを拝借して見ていきたいと思います。
Apollo Clientを使用し実装する前提で解説していきます、Apollo Clientの設定は省いています。

変更前

import React from 'react';
import { useQuery, gql } from '@apollo/client';

// モデル型
interface RocketInventory {
  id: number;
  model: string;
  year: number;
  stock: number;
}

// クエリ型
interface RocketInventoryData {
  rocketInventory: RocketInventory[];
}

// パラメータ型
interface RocketInventoryVars {
  year: number;
}

// クエリドキュメント
const GET_ROCKET_INVENTORY = gql`
  query GetRocketInventory($year: Int!) {
    rocketInventory(year: $year) {
      id
      model
      year
      stock
    }
  }
`;

export function RocketInventoryList() {
  // Apollo Clientを使用しクエリをリクエストしてレスポンスを取得
  const { loading, data } = useQuery<RocketInventoryData, RocketInventoryVars>(
    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>
  );
}

上記のコードがですね。。

変更後

import React from 'react';
import { useGetRocketInventoryQuery } from 'src/gen/graphql';

export function RocketInventoryList() {
  // Apollo Clientのカスタムフック関数でクエリをリクエストしてレスポンスを取得
  const { loading, data } = useGetRocketInventoryQuery({
    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>
  );
}

コード量を少なく実装できるようになりました!
パッと見た感じ各型定義の記述が無くなり
API呼び出しフックも短くなって見やすいですね。
次にどこがどうなったか見ていきたいと思います。

と、その前にgraphql-codegenを利用する下準備を説明します。
まず今回使用するライブラリをインストール ※Apollo Clientを使用していない場合、@graphql-codegen/typescript-react-apolloは不要です。

npm install @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

次に下記、codegen.ymlを下記の設定で用意する

schema: http://localhost:3000/graphql //エンドポイント先URL
documents: "src/**/*.graphql" // クエリ内容の保存先フォルダ
generates:
  src/gen/graphql.ts: //型定義ファイルのパス
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-react-apollo"

package.jsonに下記スクリプトを追加

"scripts": {
  "generate": "graphql-codegen --config codegen.yml"
}

これでgraphql-codegenを利用できる準備が整いました。
次に変更後のコードを記述するために必要な手順を説明します。

codegen.ymlで指定したdocumentsにGetRocketInventory.graphqlという
下記クエリドキュメントを置く

query GetRocketInventory($year: Int!) {
  rocketInventory(year: $year) {
    id
    model
    year
    stock
  }
}

最後に下記コマンドを実行する

npm run generate

此処までを行うと変更前のコードで定義されていた
各定義(モデル、パラメータなど)がgraphql.tsへ出力されます。
下記が出力された定義一覧。

export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

export type Query = {
  __typename?: 'Query';
  /** 出力されたクエリ型 */
  rocketInventory: Array<RocketInventory>;
};

// 出力されたモデル型
export type RocketInventory = {
  __typename?: 'RocketInventory';
  id: Scalars['ID'];
  model: Scalars['String'];
  stock: Scalars['Int'];
  year: Scalars['Int'];
};

// 出力されたパラメータ型
export type GetRocketInventoryQueryVariables = Exact<{
  year: Scalars['Int'];
}>;

// 出力されたレスポンス内容の型
export type GetRocketInventoryQuery = { __typename?: 'Query', rocketInventory: Array<{ __typename?: 'RocketInventory', id: string, model: string, year: number, stock: number }> };

// 出力されたクエリドキュメント
export const GetRocketInventoryDocument = gql`
    query GetRocketInventory($year: Int!) {
  rocketInventory(year: $year) {
    id
    model
    year
    stock
  }
}
    `;

/**
 * __useGetRocketInventoryQuery__
 *
 * To run a query within a React component, call `useGetRocketInventoryQuery` and pass it any options that fit your needs.
 * When your component renders, `useGetRocketInventoryQuery` returns an object from Apollo Client that contains loading, error, and data properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
 *
 * @example
 * const { data, loading, error } = useGetRocketInventoryQuery({
 *   variables: {
 *      year: // value for 'year'
 *   },
 * });
 */
// 出力されたApollo Clientのカスタムフック
export function useGetRocketInventoryQuery(baseOptions: Apollo.QueryHookOptions<GetRocketInventoryQuery, GetRocketInventoryQueryVariables>) {
        const options = {...defaultOptions, ...baseOptions}
        return Apollo.useQuery<GetRocketInventoryQuery, GetRocketInventoryQueryVariables>(GetRocketInventoryDocument, options);
      }
export function useGetRocketInventoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetRocketInventoryQuery, GetRocketInventoryQueryVariables>) {
          const options = {...defaultOptions, ...baseOptions}
          return Apollo.useLazyQuery<GetRocketInventoryQuery, GetRocketInventoryQueryVariables>(GetRocketInventoryDocument, options);
        }
export type GetRocketInventoryQueryHookResult = ReturnType<typeof useGetRocketInventoryQuery>;
export type GetRocketInventoryLazyQueryHookResult = ReturnType<typeof useGetRocketInventoryLazyQuery>;
export type GetRocketInventoryQueryResult = Apollo.QueryResult<GetRocketInventoryQuery, GetRocketInventoryQueryVariables>;

codegen.ymlで設定したエンドポイント先URLを指定することで
GraphQLサーバー側で定義されているGraphQLスキーマを基に
上記の各型定義がフロントエンド側で自動で定義されます。

また作成したクエリドキュメントを基にクエリドキュメントと
@graphql-codegen/typescript-react-apolloによってApollo Clientのカスタムフックもgraphql.tsへ出力されます。

これによって変更前コードで定義していたGraphQL API呼び出しの記述が全てgraphql.tsに定義され
それらをラップしたApollo Clientのカスタムフックが実装されることで
変更後のコードでは簡単な記述でAPIが呼び出せるようになっています。

今回は定義していないですがオリジナルスカラー型を定義した場合も
GraphQLサーバー側のスキーマを基に自動で型定義してくれます。
いやー本当に便利。
どうですか?便利ですよね。いつ使うの?。。。い。。

まとめ

毎回サーバー側のスキーマ定義と
フロントエンド側のスキーマ型定義を合わせる作業が無くなった。
(npm run generateで自動でサーバー側スキーマとクライアント側の型定義を合わせるため)
自動で定義されることでフロントエンド側の型定義の質が担保され
更にTypeScriptの型定義を行う作業が減り、他の開発に専念できてスピードも上がるとても便利なツールです。
是非、導入されていない方は導入してみてください。

参照

qiita.com

www.apollographql.com

www.oreilly.co.jp