Gatsby.js を完全TypeScript化する

機会があり Gatsby.js を書いているので、その際に TypeScript化した手順を紹介します。Gatsby.js では Contentful などを使うことが多いと思いますが、本稿はTS化の解説につき、そこは割愛します。

[サンプルリポジトリはこちら](https://github.com/takefumi-yoshii/gatsby-starter-typescript)

# Handson
[一番単純なスターター](https://www.gatsbyjs.org/starters/haysclark/gatsby-starter-typescript/)ベースで始めます。このスターターは古いので、dependencies は以下のとおり修正し、他パッケージもアップデートします。

```package.json.diff
-"@types/react": "15.0.21",
-"@types/react-dom": "0.14.23",
+"@types/react-helmet": "^5.0.14",
+"react": "^16.11.0",
+"react-dom": "^16.11.0",
+"react-helmet": "^5.2.1",
+"typescript": "^3.7.2",
```

TS3.7 の Optional Chaining があった方が良いので、以下の様な`.babelrc`を追加します。~~（今現状だと、prettier でつらみが…）~~ [v1.19.1](https://github.com/prettier/prettier/blob/master/CHANGELOG.md#1191)で対応されました🎉

```json
{
  "presets": [
    [
      "@babel/preset-env",
      { "targets": { "browsers": [">0.25%", "not dead"] }}
    ],
    "@babel/preset-react"
  ],
  "plugins": [
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-proposal-class-properties"
  ]
}
```

# ComponentsをTSX化する
Components を TSX で書くために、`gatsby-plugin-typescript`を使います。本家のスターターでも、すでに gatsby-config.js に追加されています。

```gatsby-config.js
plugins: [
  ...,
  `gatsby-plugin-typescript`
]
```

# Query型を自動生成する
`gatsby-plugin-graphql-codegen`を使います。srcディレクトリ内で `import { graphql } from "gatsby"` されているファイルの query 定義から、型定義が自動生成されます。

```gatsby-config.js
plugins: [
  ...,
  {
    resolve: 'gatsby-plugin-graphql-codegen',
    options: {
      fileName: `types/graphql-types.d.ts`
    }
  }
]
```

query には、プロジェクト一意の名称を付与します（名称が競合しているとビルドに失敗します）。例えば、リスト1の様な query は、リスト2の様な型定義が自動出力されます。XXX という名称は、XXXQuery になるということです。（分かりやすく IndexHoge としていますが、基本的にはディレクトリ由来の名称でアッパーキャメルで命名すると良さそうです）

```【リスト1】pages/index.tsx
export const pageQuery = graphql`
  query IndexHoge {
    site {
      siteMetadata {
        title
      }
    }
  }
`
```

```【リスト2】types/graphql-types.d.ts
export type IndexHogeQuery = {
  site: Maybe<{ siteMetadata: Maybe<Pick<SiteSiteMetadata, "title">> }>
}
```

【※注意】この自動生成される型定義ファイルは、**src ディレクトリ以外に出力される様に指定しなければいけません。**src ディレクトリ内の変更を監視しているので、src 以下に出力すると、ずっと出力され続けてしまいます。

# 自動生成された型を利用する

自動生成された`IndexHogeQuery`型を、該当ページで使います。

```tsx:src/pages/index.tsx
import * as React from "react"
import { Link } from "gatsby"
import { graphql } from "gatsby"
import { IndexHogeQuery } from "../../types/graphql-types"
// ______________________________________________________
//
type Props = {
  data: IndexHogeQuery
}
// ______________________________________________________
//
const Component: React.FC<Props> = ({ data }) => (
  <div>
    <h1>Hi people</h1>
    <p>
      Welcome to your new{" "}
      <strong>{data.site?.siteMetadata?.title}</strong> site.
    </p>
    <p>Now go build something great.</p>
    <Link to="/page-2/">Go to page 2</Link>
  </div>
)
// ______________________________________________________
//
export const pageQuery = graphql`
  query IndexHoge {
    site {
      siteMetadata {
        title
      }
    }
  }
`
// ______________________________________________________
//
export default Component
```

query 名称などを変更すると型が再生成され、そんな型ないよ、と怒られるはずです。


# gatsby-node.js を TS化する
動的ページを生成する場合、gatsby-node.js で createPages 関数を定義し、exports する必要があります。このとき、Promise を扱う非同期コードが基本です。async/await はもちろん、型推論も欲しくなるので、ここもTypeScript化します。`ts-node`を使っていきます。

```package.json.diff
+"ts-node": "^8.4.1",
```

エントリーポイントになる gatsby-node.js は拡張子はそのままでよく、なかで ts-node を register し、そこから tsファイルのエントリーポイントを読み込みます。

```gatsby-node.js
'use strict'
require('ts-node').register({
  compilerOptions: {
    module: 'commonjs',
    target: 'esnext',
  },
})
exports.createPages = require('./gatsby-node/index').createPages
```

それでは中の実装を見ていきます。GraphQL に使う query 文字列は次の通りで、`gatsby-config.js`から抽出できる、`siteMetadata`を利用します。

```gatsby-node/index.ts
const query = `
{
  site {
    siteMetadata {
      title
      authors {
        name
        slug
      }
    }
  }
}
`
```

`gatsby-config.js`はこの様になっています。便宜上、この`gatsby-config.js`のデータを利用していますが、Contentful のデータなどでも構いません。

```gatsby-config.js
module.exports = {
  siteMetadata: {
    title: `Gatsby Typescript Starter`,
    authors: [
      { name: 'Tori', slug: 'tori' },
      { name: 'Neko', slug: 'neko' },
      { name: 'Inu', slug: 'inu' }
    ]
  }
}
```

次のコードが`createPage`するまでの全容です。`Result`型と`AuthorPageContext`型を、それぞれ`graphql`関数と`createPage`関数に Generics で指定しているところがポイントです。

```gatsby-node/index.ts
import path from "path"
import { GatsbyNode } from "gatsby"
import { Site, SiteSiteMetadataAuthors } from "../types/graphql-types"
// ______________________________________________________
//
type Result = {
  site: Site
}
export type AuthorPageContext = {
  author: SiteSiteMetadataAuthors
} // template で利用するため export
// ______________________________________________________
//
const query = // ...(略)
export const createPages: GatsbyNode["createPages"] = async ({
  graphql,
  actions: { createPage }
}) => {
  const result = await graphql<Result>(query)
  if (result.errors || !result.data) {
    throw result.errors
  }
  const { siteMetadata } = result.data.site
  if (!siteMetadata || !siteMetadata.authors) {
    throw new Error("undefined authors")
  }

  for (let author of siteMetadata.authors) {
    if (author) {
      createPage<AuthorPageContext>({
        path: `/authors/${author.slug}/`,
        component: path.resolve("src/templates/author.tsx"),
        context: { author }
      })
    }
  }
}
```

# template の pageContext 型を縛る

`gatsby-node/index.ts`で、`AuthorPageContext`型を定義したので、それを利用します。これで、`createPage`関数と template を紐づけることができました。

```tsx:src/templates/author.tsx
import * as React from "react"
import { Link } from "gatsby"
import { AuthorPageContext } from "../../gatsby-node"
// ______________________________________________________
//
type Props = {
  pageContext: AuthorPageContext
}
// ______________________________________________________
//
const Component: React.FC<Props> = ({ pageContext }) => (
  <div>
    <h1>Author name is {pageContext.author.name}</h1>
    <ul>
      <li><Link to="/authors/">Back to authors</Link></li>
      <li><Link to="/">Back to top</Link></li>
    </ul>
  </div>
)
// ______________________________________________________
//
export default Component
```


# Contentful を使う場合

gatsby-source-contentful を利用します。開発サーバー立ち上げ時に Contentful の全データを取得してくるので、GraphiQL も型定義も全て定義が伝播します。開発効率がかなり良いので、是非お試しください。