💁

GraphQL Code Generator(graphql-codegen) おすすめ設定 for TypeScript

2022/10/03に公開

https://zenn.dev/layerx/articles/028cb518cffd61


全体

生成物をフォーマッタにかける

graphql-codegen には Lifecycle Hooks という仕組みがあり、いくつかの用意された hook ポイントで任意のコマンドを実行できる。
これを使って生成されたコードを Prettier 等のフォーマッタに通しておくのがおすすめ。

hooks:
  afterAllFileWrite:
    - prettier --write

基本 DO NOT EDIT とはいえ、コードジャンプしてきて生成された型を読みたいケースはよくある。
そういうときのために、人間が見やすいように改行しておいてもらうといい。

typescript plugin, typescript-operation plugin

最も基本のプラグイン。ちなみに typescript はスキーマ、typescript-operation は Operation(Query, Mutation, Fragment)に関心がある。
設定できる項目は共通のものが多いのでまとめて記載。

immutableTypes: 意図しないデータ更新を防ぐ

生成される型のフィールドに readonly がつき、配列は ReadonlyArray になる。
「クエリ結果を弄ってしまって意図しない箇所に影響が出る」みたいなのを型のレベルで防ぐことができる。

また、関数の引数では readonly であること == 引数を書き換えないことの宣言 になるので、そういう観点でもなるべく readonly にしておきたい。
たまにライブラリが提供する関数の型定義で readonly ついてなくて面倒になるケースがあるけど、それはその型定義に改善余地ありなので PR を投げてあげましょう。

https://qiita.com/uhyo/items/0fd033ff1aed9b4b32dd

defaultScalarType: custom scalar を any にしない

Custom Scalar はデフォルトでは any になるが、これは最悪なので別の型にマッピングしよう!という設定。

よくあるケースでは DateDateTime のような custom scalar を定義したんだけど、any になっていることで JS の Date と勘違いして getFullYear() 呼んじゃってバグる みたいなの。

一番お手軽なのはこの defaultScalarType で、これを unknown にしておくことで、前述したようなバグは型チェックで落とせる。

scalars, strictScalars: custom scalar を型で区別できるようにする

defaultScalarType の代わりに入れる、より高度な設定。

scalars は各 custom scalar ごとにマッピング先の TypeScript の型を指定できる。
たとえば DateString のような Branded Type をマッピング先に指定することで、unknown よりもドキュメント・インタフェースとして情報量を増やすことができる。
https://www.wantedly.com/companies/wantedly/post_articles/387161

(余談だけど、上記の記事では適当なフィールドを定義して Branded Type を作っているが、unique symbol を使うほうがより安全という話があるらしい。)

一方で、scalars で指定が漏れた custom scalar はデフォルトにフォールバックされてしまう。それを防ぐのが strictScalars。Branded Type を使うなら全部の custom scalar に指定しない理由はないので、scalarsstrictScalars はセットでの運用が基本になる。

enumsAsConst: TypeScript の enum を使わない

デフォルトだと GraphQL Enum に対して TypeScript の Enum が出力されるが、それを as const を使った union 型に変更するオプション。
TypeScript の Enum はいろいろ悩ましいところが多いので、できるだけ利用を避けていきたい。

https://typescriptbook.jp/reference/values-types-variables/enum/enum-problems-and-alternatives-to-enums

https://engineering.linecorp.com/ja/blog/typescript-enum-tree-shaking/

skipTypename: __typename を使うときは明示しよう

Operation 中で指定しない限りは__typename が型に出てこなくなる。
__typename が暗黙的に取得されてるかどうかは Client 実装によって異なるので、「自身のコード中で __typename を使うときは明示的に指定すべき」というルール強制したい、というのがモチベーション。

avoidOptionals: undefined を厳密に扱う

nullable なフィールドが optional になるのを防ぐ。
デフォルトの挙動だとfield?: Maybe<T> だが、avoidOptionalfield: Maybe<T> になる。
指定したフィールドは null だったら素直に null 返してくる(省略されない)のであれば、実際の挙動に沿った型にしておきたい、という意図。

なお、GraphQL の引数は「値を入れない」と「null を渡す」を区別できるので、Input では avoidOptionals は無効化しておくほうがいい場合がある。

https://zenn.dev/izumin/articles/710eefc8172f66

avoidOptionals:
  field: true
  inputValue: false
  object: true
  defaultValue: false

(これ書いてて気になった: @skip@include が指定されてるフィールドはどういう挙動になるんだっけ?)

add plugin

任意の文字列を挿入できるプラグイン。

eslint-disableDO NOT EDIT などのコメントを追加

graphql-codegen の出力にはヘッダコメントがつかないので、自分でつける必要がある。
自動生成であることをアピールするための DO NOT EDIT や、ESLint の対象外であることを宣言する /* eslint-disable */ の追加がよくあるパターン。

plugins:
  - add:
      content: "// Code generated by graphql-codegen. DO NOTT EDIT."

scalars オプションで使う Branded Type の定義を差し込む

scalars のところで言及した Branded Type の指定だが、そもそもその型を生成ファイル内で使えないと話にならない。
なので、 add plugin を使って Branded Type の定義を挿入してあげる。

plugins:
  - add:
      content: '// Code generated by graphql-codegen. DO NOT EDIT.'
  - add:
      content: 'export type DateString = string & { readonly brand: unique symbol };'
  - typescript
config:
  scalars:
    Date: DateString

別のファイルで Branded Type を定義して import してくるのでもいいかもしれない。

typed-document-node plugin

GraphQL Document の TypedDocumentNode を生成する。
詳しくは以下の記事や動画を見てもらえばいいが、簡単に言うと「TypedDocumentNodeuseQuery に渡すだけで variables やレスポンスに型がつく」もの。

https://the-guild.dev/blog/typed-document-node
https://graphql.wtf/episodes/41-typed-document-node

JS の GraphQL エコシステムはだいたい TypedDocumentNode に対応してるので、特定のライブラリのラッパー関数を生成する plugin を使うよりも多くの場合で typed-document-node plugin のほうがいいはず。

typedscript-msw plugin

名前の通り、MSW のハンドラを定義するためのヘルパを生成してくれる。
だいたいこんなかんじ(例は plugin ページから引用)。

// mockGetUserQuery が生成される関数
mockGetUserQuery((req, res, ctx) => { 
  const { id } = req.variables // ← 型がついてる
 
  return res(
    ctx.data({
      getUser: { name: 'John Doe', id } ← 型がついてる
    })
  )
})

テストや Storybook のお供にどうぞ。

near-operation-files preset

GraphQL Operation に対応する生成物を、その Operation が記述されてるファイルの近くに書き出してくれる。

  • Query や Fragment を使う場所の近くに型定義がある方が見やすい
  • ひとつひとつの生成ファイルが小さくなって見やすい

というような生産性観点のメリットもあるし、以下の記事にあるようなパフォーマンス上のメリットもある。事情がない限りは入れておきたい。

https://blog.hiroppy.me/entry/2021/08/12/092839

gql-tag-operations preset

これは超特殊だし発展途上だけど非常に良いものなので紹介… と言いたいところだが、話がかなり逸れるので別記事で紹介します。
次期メジャーである GraphQL Code Generator v3 はこの gql-tag-operations の方向に進化しそうな雰囲気もあるので楽しみですね。

https://github.com/dotansimha/graphql-code-generator/issues/8296

さいごに

みなさんのオススメ設定もあれば是非コメントして下さい!

Discussion