Gatsby+TypeScriptを快適にするためのgatsby-plugin-graphql-codegenの設定

Gatsby + TypeScript の構成で GraphQL の型定義を自動生成するための [gatsby-plugin-graphql-codegen](https://www.gatsbyjs.org/packages/gatsby-plugin-graphql-codegen/) というライブラリがあります。
これを使用すると GraphQL から取得したデータに自動で型が付与されてかなり快適に開発できるようになるのですが、一部困ったことが出てきたので、概要と解決策を記事にまとめます。

ちなみに Gatsby + TypeScript の環境を構築するにはこちらの記事が非常に参考になります。
[Gatsby.js を完全TypeScript化する - Qiita](https://qiita.com/Takepepe/items/144209f860fbe4d5e9bb)

# 環境

- TypeScript `3.9.7`
- React `16.13.1`
- Gatsby `2.4.13`
- gatsby-plugin-graphql-codegen `2.7.1`

# 結論

結論から言うと、`gatsby-config.js(.ts)`でプラグインを読み込む際に以下の設定をすると幸せになれます。

```js:gatsby-config.js
module.exports = {
  plugins: [
    // ...中略
    {
      resolve: 'gatsby-plugin-graphql-codegen',
      options: {
        codegenConfig: { maybeValue: 'T | undefined' }, // これを追加！
      },
    },
  ]
}
```

# どういうこと？

`gatsby-plugin-graphql-codegen`は、GraphQL クエリから型生成する際、全ての戻り値を以下の`Maybe`という型でラップします。

```ts:graphql-types.ts
export type Maybe<T> = T | null;
```

GraphQL から取得するデータは`null`になる可能性があるので、これは妥当な型定義ではあります。
TypeScript では Optional Chaining`?.`を利用することで、このような Nullable な値に対しても安全に値を取得できることができます。

## 例

Gatsby のデフォルトスターター [gatsby-starter-default](https://www.gatsbyjs.org/starters/gatsbyjs/gatsby-starter-default/) を例にとると、以下のように GraphQL クエリで画像を読み込んでいるコンポーネントがあります。

```jsx:src/components/image.js
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"

const Image = () => {
  const data = useStaticQuery(graphql`
    query {
      placeholderImage: file(relativePath: { eq: "gatsby-astronaut.png" }) {
        childImageSharp {
          fluid(maxWidth: 300) {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }
  `)

  return <Img fluid={data.placeholderImage.childImageSharp.fluid} />
}

export default Image
```

これを TypeScript 化すると以下のように書くことができます。

```tsx:src/components/image.tsx
import React from "react"
import { useStaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"
import { ImageQuery } from "../../graphql-types"

const Image: React.FC = () => {
  const data = useStaticQuery<ImageQuery>(graphql`
    query Image {
      placeholderImage: file(relativePath: { eq: "gatsby-astronaut.png" }) {
        childImageSharp {
          fluid(maxWidth: 300) {
            ...GatsbyImageSharpFluid
          }
        }
      }
    }
  `)

  return <Img fluid={data.placeholderImage?.childImageSharp?.fluid} />
}

export default Image
```

`gatsby-plugin-graphql-codegen`は、`query`に名前を付けると（上の場合`query Image`）`ImageQuery`という名前で`graphql-types.ts`に型定義を自動生成してくれます。
それを`useStaticQuery`に型指定してあげることで`data`が型補完されます。
`Img`コンポーネントに対しては Optional Chainging を使用して値を渡すことで、画像が取得できなかった場合でも実行時エラーにならずに処理してくれます。

# 起きたこと

しかしここで`tsconfig.json`の指定によっては[^1]以下のようなコンパイルエラーが発生します。

[^1]: `"strict": true`にしている等

```
この呼び出しに一致するオーバーロードはありません。
  2 中 1 のオーバーロード, '(props: Readonly<GatsbyImageProps>): GatsbyImage' により、次のエラーが発生しました。
    型 'Pick<ImageSharpFluid, "base64" | "aspectRatio" | "src" | "srcSet" | "sizes"> | null | undefined' を
型 'FluidObject | FluidObject[] | undefined' に割り当てることはできません。
      型 'null' を型 'FluidObject | FluidObject[] | undefined' に割り当てることはできません。
  2 中 2 のオーバーロード, '(props: GatsbyImageProps, context?: any): GatsbyImage' により、次のエラーが発生しました。
    型 'Pick<ImageSharpFluid, "base64" | "aspectRatio" | "src" | "srcSet" | "sizes"> | null | undefined' を
型 'FluidObject | FluidObject[] | undefined' に割り当てることはできません。
      型 'null' を型 'FluidObject | FluidObject[] | undefined' に割り当てることはできません。
```

要点を抜き出すと「`null`を`undefined`に割り当てることはできません」と言っています。
`gatsby-image`コンポーネントの`Img`は`undefined`を受け付けるように型定義されているのですが、`null`は受け取ってくれないようです。
先程述べた通り`gatsby-plugin-graphql-codegen`は全ての型を`Maybe<T> = T | null`でラップするので、`data.placeholderImage?.childImageSharp?.fluid`は`null`になりうると判断されてしまいます。
これは、以下のように`null`だったら`undefined`になるように書けば回避できます。

```tsx:src/components/image.tsx
const Image: React.FC = () => {
  // ...略
  return <Img fluid={data.placeholderImage?.childImageSharp?.fluid ?? undefined} />
}

export default Image
```

ただ正直 GraphQL から取得したデータ全てに対してこれを行うのは結構大変です。特に React コンポーネントの Optional な`props`の定義は`T | undefined`であることが多いので、結構な頻度で`null -> undefined`の変換が発生してきます。

# Maybe の型を変える

そこで冒頭の結論に戻りますが、プラグイン読み込み時に以下の設定を記載します。

```js:gatsby-config.js
module.exports = {
  plugins: [
    // ...中略
    {
      resolve: 'gatsby-plugin-graphql-codegen',
      options: {
        codegenConfig: { maybeValue: 'T | undefined' }, // これを追加！
      },
    },
  ]
}
```

`codegenConfig.maybeValue`で、生成される`Maybe`の定義をオーバーライドできます。
`T | null`の代わりに`T | undefined`とすることで、Nullable な値が`undefined`に統一され、シンプルな Optional Chaining だけで書けるようになります。

```tsx:src/components/image.tsx
const Image: React.FC = () => {
  // ...略
  return <Img fluid={data.placeholderImage?.childImageSharp?.fluid} />
}

export default Image
```

# 型定義を変えて大丈夫なの？

`null`を勝手に`undefined`と扱ってしまって大丈夫なのかと思うかもしれませんが、基本的に問題はないと思っています。
型定義を変えるだけなので、少なくともトランスパイル後の JavaScript には影響はありません。
最近のライブラリなら`null`と`undefined`の違いで大きな問題が起こることはないと思っていますが、全てを確認したわけではないです。
ダメだった場合はごめんなさい。

# まとめ

- `gatsby-plugin-graphql-codegen`の生成する`null`に悩まされている方は`codegenConfig: { maybeValue: 'T | undefined' }`を設定すると快適になるかもしれません