機会があり Gatsby.js を書いているので、その際に TypeScript化した手順を紹介します。Gatsby.js では Contentful などを使うことが多いと思いますが、本稿はTS化の解説につき、そこは割愛します。
Handson
一番単純なスターターベースで始めます。このスターターは古いので、dependencies は以下のとおり修正し、他パッケージもアップデートします。
-"@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 に追加されています。
plugins: [
...,
`gatsby-plugin-typescript`
]
Query型を自動生成する
gatsby-plugin-graphql-codegen
を使います。srcディレクトリ内で import { graphql } from "gatsby"
されているファイルの query 定義から、型定義が自動生成されます。
plugins: [
...,
{
resolve: 'gatsby-plugin-graphql-codegen',
options: {
fileName: `types/graphql-types.d.ts`
}
}
]
query には、プロジェクト一意の名称を付与します(名称が競合しているとビルドに失敗します)。例えば、リスト1の様な query は、リスト2の様な型定義が自動出力されます。XXX という名称は、XXXQuery になるということです。(分かりやすく IndexHoge としていますが、基本的にはディレクトリ由来の名称でアッパーキャメルで命名すると良さそうです)
export const pageQuery = graphql`
query IndexHoge {
site {
siteMetadata {
title
}
}
}
`
export type IndexHogeQuery = {
site: Maybe<{ siteMetadata: Maybe<Pick<SiteSiteMetadata, "title">> }>
}
【※注意】この自動生成される型定義ファイルは、**src ディレクトリ以外に出力される様に指定しなければいけません。**src ディレクトリ内の変更を監視しているので、src 以下に出力すると、ずっと出力され続けてしまいます。
自動生成された型を利用する
自動生成されたIndexHogeQuery
型を、該当ページで使います。
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
を使っていきます。
+"ts-node": "^8.4.1",
エントリーポイントになる gatsby-node.js は拡張子はそのままでよく、なかで ts-node を register し、そこから tsファイルのエントリーポイントを読み込みます。
'use strict'
require('ts-node').register({
compilerOptions: {
module: 'commonjs',
target: 'esnext',
},
})
exports.createPages = require('./gatsby-node/index').createPages
それでは中の実装を見ていきます。GraphQL に使う query 文字列は次の通りで、gatsby-config.js
から抽出できる、siteMetadata
を利用します。
const query = `
{
site {
siteMetadata {
title
authors {
name
slug
}
}
}
}
`
gatsby-config.js
はこの様になっています。便宜上、このgatsby-config.js
のデータを利用していますが、Contentful のデータなどでも構いません。
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 で指定しているところがポイントです。
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 を紐づけることができました。
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 も型定義も全て定義が伝播します。開発効率がかなり良いので、是非お試しください。