理系学生日記

おまえはいつまで学生気分なのか

GraphQL Code Generatorを使ってTypeScript型定義を自動生成することでGraphQLの開発が楽になる

型のない言語でJSONのような構造を扱おうとすると、このオブジェクトのフィールドには何が入っていたっけ、というのを IDEとドキュメントを行ったり来たりしながら確認する羽目になります。 これはGraphQLでも同様でした。TypeScriptで開発をしていたとしても、そもそも型定義を書くのがつらい。 結果としてanyを頻出させることになったり、存在しないフィールドを参照していたバグを特定するためにconsole.logデバッグしたりと なかなか苦しめられました。

そんなときに導入して体験が一気に変わったのが、GraphQL Code Generatorでした。

どうなるのか

これが導入後の状況です。GraphQLのレスポンスの型定義をベースに、補完も効いていることがわかります。

ごくごく一部ですが、GraphQLのクエリに対して生成された型定義です。

export type MergeRequestCommentsQuery = { __typename?: 'Query', mergeRequest?: { __typename?: 'MergeRequest', title: string, createdAt: any, mergedAt?: any | null, author?: { __typename?: 'MergeRequestAuthor', id: string, name: string } | null, approvedBy?: { __typename?: 'UserCoreConnection', edges?: Array<{ __typename?: 'UserCoreEdge', node?: { __typename?: 'UserCore', id: string, name: string } | null } | null> | null } | null, discussions: { __typename?: 'DiscussionConnection', edges?: Array<{ __typename?: 'DiscussionEdge', node?: { __typename?: 'Discussion', notes: { __typename?: 'NoteConnection', nodes?: Array<{ __typename?: 'Note', id: any, system: boolean, body: string, url?: string | null, author: { __typename?: 'UserCore', id: string, name: string } } | null> | null } } | null } | null> | null, pageInfo: { __typename?: 'PageInfo', endCursor?: string | null, hasNextPage: boolean } } } | null };

GraphQL Code Generator

設定ファイル

GraphQL Code Generatorは、codegen.yaml(codegen.jsonでもOK)という設定ファイルの定義に基づいて動作します。 ぼくの場合は、以下のようにしています。

overwrite: true
schema: "https://gitlab.com/api/graphql"
documents: "src/**/*.graphql"
generates:
  src/generated/graphql.ts:
    plugins:
      - "typescript"
      - "typescript-operations"
      - "typescript-document-nodes"
    config:
      namingConvention:
        enumValues: keep

schemaでGraphQLのエンドポイント、documentsでGraphQLドキュメントの場所を定義します。 GraphQLドキュメントの例(merge-requests.graphql)は以下のようなものです。

query mergeRequestIds(
  $fullPath: ID!
  $targetBranches: [String!]
  $afterMergeRequest: String
) {
  project(fullPath: $fullPath) {
    id
    mergeRequests(
      state: merged
      targetBranches: $targetBranches
      after: $afterMergeRequest
    ) {
      pageInfo {
        endCursor
        hasNextPage
      }
      edges {
        node {
          id
        }
      }
    }
  }
}

generatesが実際の型定義の生成にまつわるオプションで、生成ファイル名をキーとして、その生成時のオプションを記述していきます。 ここで使っているプラグインはそれぞれ以下です。

プラグイン 概要
typescript GraphQLをTypeScriptで扱う時の基盤となる型定義を生成する
typescript-operation GraphQLドキュメント(querymutationfragment等)に対する型定義を生成する

このほかtypescript-document-nodesも使っていますが、 僕自身がきちんとその目的を正確に読み解けていないです。 ただ、Apollo Clientを使っている一方で、HooksであるuseQuery ではなくqueryを使いたかったので利用しました。

コード生成

設定ファイルを引数にgraphql-codegenコマンドを実行すると、自動的に型定義が生成されます。

$ graphql-codegen --config codegen.yml

使ってみる

僕自身はGraphQL ClientとしてApollo Clientを利用しています。

自動生成された型定義をインポートの上、以下のようにしてクエリを実行するだけで冒頭に記載した補完がなされるようになりました。

import {
    MergeRequestIdsQuery,
    MergeRequestCommentsQuery,
    MergeRequestIds,
    MergeRequestComments,
} from "./generated/graphql";

(snip)

    const result: ApolloQueryResult<MergeRequestCommentsQuery> =
      await client.query({
        query: MergeRequestComments,
        variables: {
          mergeRequestId: mergeRequestId,
          discussionsAfter: cursor,
        },
      });

やはりこの型定義があるとないとでは開発効率が雲泥の差です。 TypeScript、というよりは型の助けがあると、ずいぶんとGraphQL開発は楽になりそうです。