Edited at

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