型のない言語で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ドキュメント(query やmutation 、fragment 等)に対する型定義を生成する |
このほか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開発は楽になりそうです。