Help us understand the problem. What is going on with this article?

Gatsby.js を完全TypeScript化する

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

サンプルリポジトリはこちら

Handson

一番単純なスターターベースで始めます。このスターターは古いので、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で対応されました🎉

{
  "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型を、該当ページで使います。

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 を紐づけることができました。

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 も型定義も全て定義が伝播します。開発効率がかなり良いので、是非お試しください。

Takepepe
Web Application Developer. interested in TypeScript AST.
dena_coltd
    Delight and Impact the World
https://dena.com/jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away