チュートリアル4~8までの内容をTypeScript化してみた。Data in Gatsbyより
GraphQL周りの型はgatsby-plugin-typegenというプラグインで型生成できるため楽に実装できた。
(Gatsby.jsのTypeScript化 2020を大いに参考。Zennのサイトへ飛びます。)
※ただし1箇所だけ最後まで分からなかった問題はあり(必要な箇所で後述します)
全体として動かすために最低限必要なTS化となっており細かいところはどうか許してやってください(指摘はもちろんもらえたら嬉しいです)
成果物
TS化→http://localhost:9000/ でも同様に立ち上げることができたという画像
チュートリアルをデフォルト(JavaScript)で終えたブランチはココ
チュートリアルをTS化したブランチ(完成形)はココ
手順
- typescriptの環境構築
- GraphQLの型生成
- 全ページのTS化
1. typescriptの環境構築
"tsc --init"で初期化されたtsconfig.jsonファイルを作成します。
npx tsc --init
次にtypescriptに必要なライブラリを2つ落としてきます。
yarn add -D typescript
yarn add gatsby-plugin-typegen // gatsby-config.jsのplugins[]へ追記
tsconfig.jsonの設定
最初はstrictモードのオプションを外しておきます。
{
"compilerOptions": {
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["dom", "es2017"], /* Specify library files to be included in the compilation. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"outDir": "./build", /* Redirect output structure to the directory. */
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"baseUrl": "src", /* Base directory to resolve non-absolute module names. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
// "strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
},
"include": ["src/**/*", "gatsby-node/index.ts"],
"exclude": ["node_modules", "public", "build", "src/templates/blog-post.tsx"],
}
gatsby-config.jsへ追記
module.exports = {
siteMetadata: {
title: `typescriptのテスト gatsbyのチュートリアル参考:https://www.gatsbyjs.com/tutorial/part-four/`,
description: `これは説明文章ですよ`,
author: `gatsbyJSマン`,
},
plugins: [
`gatsby-plugin-emotion`,
{
resolve: `gatsby-plugin-typography`,
options: {
pathToConfigModule: `src/utils/typography`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
name: `src`,
path: `${__dirname}/src/`,
}
},
`gatsby-transformer-remark`,
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `GatsbyJS`,
short_name: `GatsbyJS`,
start_url: `/`,
background_color: `#6b37bf`,
theme_color: `#6b37bf`,
display: `standalone`,
icon: `src/images/icon.png`,
},
},
`gatsby-plugin-offline`,
`gatsby-plugin-react-helmet`,
`gatsby-plugin-typegen`, // 型生成のプラグインを追加
],
}
2. GraphQLの型生成
どこからでも良いのですが、まずindex.jsのファイルでGraphQLの型を生成してみます。
index.tsxへファイル名を変更。当然ですがそのままだと型エラーが表示されます(エディターはVSCodeを使用)。
下記のように書き換えます。
/** @jsx jsx */ //emotionCSSのTS化のための記述
import React, {FC} from "react"
import { jsx, css } from '@emotion/react' // jsxを追加
import { Link, graphql } from "gatsby"
import { rhythm } from "../utils/typography"
import Layout from "components/layout"
const Home: FC<{ data: any }> = ({data}): any => { //型生成するまで適当にanyを突っ込んでおく
return (
<Layout>
<div>
<h1
css={css`
display: inline-block;
border-bottom: 1px solid;
`}
>
Amazing Pandas Eating Things
</h1>
<h4>{data.allMarkdownRemark.totalCount} Posts</h4>
{data.allMarkdownRemark.edges.map(({ node }) => (
<div key={node.id}>
<Link
to={node.fields.slug}
css={css`
text-decoration: none;
color: inherit;
`}
>
<h3
css={css`
margin-bottom: ${rhythm(1 / 4)};
`}
>
{node.frontmatter.title}{" "}
<span
css={css`
color: #bbb;
`}
>
— {node.frontmatter.date}
</span>
</h3>
<p>{node.excerpt}</p>
</Link>
</div>
))}
</div>
</Layout>
)
}
export const query = graphql`
query MarkdownOfIndex { //便宜上、任意で型の名前をつけておく(ただし、名前がなくても生成されるファイルの type Query オブジェクトの中に型が格納されているので取り出せば良い)
allMarkdownRemark {
totalCount
edges {
node {
id
frontmatter {
title
date(formatString: "DD MMMM, YYYY")
}
fields {
slug
}
excerpt
}
}
}
}
`
export default Home
この状態で、
gatsby build
してあげると、srcディレクトリ直下に __ generated __ ファイルが生成されて中に型情報ファイルが入っています。
ファイル検索で"MarkdownOfIndex"をかけてみると生成された型を確認できるのでこれを引っ張ってあげましょう。
type MarkdownOfIndexQueryVariables = Exact<{ [key: string]: never; }>;
type MarkdownOfIndexQuery = { readonly allMarkdownRemark: ( //この型を引っ張ってあげる
Pick<MarkdownRemarkConnection, 'totalCount'>
& { readonly edges: ReadonlyArray<{ readonly node: (
Pick<MarkdownRemark, 'id' | 'excerpt'>
& { readonly frontmatter: Maybe<Pick<MarkdownRemarkFrontmatter, 'title' | 'date'>>, readonly fields: Maybe<Pick<MarkdownRemarkFields, 'slug'>> }
) }> }
) };
GatsbyのPagePropsライブラリを使う
index.tsxに戻りPagePropsを追加
import { Link, graphql, PageProps } from "gatsby"
そして下記のように型を嵌めれば上手くいく。
const Home: FC<PageProps<GatsbyTypes.MarkdownOfIndexQuery>> = ({data}) => {
return (
<Layout>
//以下略
3. srcの全ページのTS化
2.と同じ要領で生成した型を当てていきます。
build前の状態
それぞれのファイルを最低限の書き換えでbuildできる状態にします。この段階でもし怒られた箇所があれば適当にanyなどをはめて一時凌ぎを。
/** @jsx jsx */
import React, {FC} from "react"
import { jsx, css } from "@emotion/react"
import { useStaticQuery, Link, graphql } from "gatsby"
import { rhythm } from "../utils/typography"
const Layout = ({ children }) => {
const data = useStaticQuery<GatsbyTypes.LayoutSiteMetadataQuery>(
graphql`
query LayoutSiteMetadata {
site {
siteMetadata {
title
}
}
}
`
)
return (
<div
css={css`
margin: 0 auto;
max-width: 700px;
padding: ${rhythm(2)};
padding-top: ${rhythm(1.5)};
`}
>
<Link to={`/`}>
<h3
css={css`
margin-bottom: ${rhythm(2)};
display: inline-block;
font-style: normal;
`}
>
{data.site.siteMetadata.title}
</h3>
</Link>
<Link
to={`/about/`}
css={css`
float: right;
`}
>
About
</Link>
{children}
</div>
)
}
export default Layout
import React from "react"
import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
const SEO = ({ description, lang, meta, title }) => {
const { site } = useStaticQuery<GatsbyTypes.SEOsiteMetadataQuery>(
graphql`
query SEOsiteMetadata {
site {
siteMetadata {
title
description
author
}
}
}
`
)
const metaDescription = description || site.siteMetadata.description
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site.siteMetadata.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site.siteMetadata.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
SEO.defaultProps = {
lang: `en`,
meta: [],
description: ``,
}
SEO.propTypes = {
description: PropTypes.string,
lang: PropTypes.string,
meta: PropTypes.arrayOf(PropTypes.object),
title: PropTypes.string.isRequired,
}
export default SEO
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
const About = ({ data }) => {
return (
<Layout>
<h1>About {data.site.siteMetadata.title}</h1>
<p>
We are the only site running on your computer dedicated to showing the
best photos and videos of pandas eating lots of food.
</p>
</Layout>
)
}
export const query = graphql`
query AboutsiteMetadata {
site {
siteMetadata {
title
}
}
}
`
export default About
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
const MyFiles = ({data}) => {
console.log(data)
return (
<Layout>
<div>
<h1>My Sites Files</h1>
<table>
<thead>
<tr>
<th>relativePath</th>
<th>prettySize</th>
<th>extension</th>
<th>birthTime</th>
</tr>
</thead>
<tbody>
{data.allFile.edges.map(({ node }, index) => (
<tr key={index}>
<td>{node.relativePath}</td>
<td>{node.prettySize}</td>
<td>{node.extension}</td>
<td>{node.birthTime}</td>
</tr>
))}
</tbody>
</table>
</div>
</Layout>
)
}
export const query = graphql`
query {
allFile {
edges {
node {
relativePath
prettySize
extension
birthTime(fromNow: true)
}
}
}
}
`
export default MyFiles
import React from "react"
import { graphql } from 'gatsby'
import Layout from "../components/layout"
import SEO from "../components/seo"
const BlogPost = ({data}) => {
const post = data.markdownRemark
return (
<Layout>
<SEO title={post.frontmatter.title} description={post.excerpt} />
<div>
<h1>{post.frontmatter.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.html }}/>
</div>
</Layout>
)
}
export const query = graphql`
query($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
}
excerpt
}
}
`
export default BlogPost
typography.jsはtypography.tsへファイル名のみ変更
gatsby build
gatsby-node.jsのTS化
次にgatsby-node.jsをTS化していくのでts-nodeをインストール
yarn add -D ts-node
gatsby-node.jsのTS化においては、gatsby-node.jsのファイル名はそのままにして、別にTS用のディレクトリとファイルである gatsby-node/index.ts を親ディレクトリ上に作ってここから引っ張ってきました。
参考:Gatsby.jsのTypeScript化 2020
まずはindex.ts上にgatsby-node.jsと同じ処理をTypeScript化させます。
import path from 'path'
import { createFilePath } from "gatsby-source-filesystem"
import { GatsbyNode } from 'gatsby'
export const onCreateNode: GatsbyNode["onCreateNode"] = ({ node, getNode, actions }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const slug = createFilePath({ node, getNode, basePath: `pages` })
createNodeField({
node,
name: `slug`,
value: slug,
})
}
}
export const createPages: GatsbyNode["createPages"] = async ({ graphql, actions }) => {
const { createPage } = actions
const result = await graphql<{ allMarkdownRemark: GatsbyTypes.Query["allMarkdownRemark"]}>(`
{
allMarkdownRemark {
edges {
node {
fields {
slug
}
}
}
}
}
`)
const { data } = result || 'undefined';
if( data === undefined) throw 'データが見つかりませんでした';
data.allMarkdownRemark.edges.forEach(({node}) => {
if(node.fields){
createPage({
path: node.fields.slug || '/undefined',
component: path.resolve(`./src/templates/blog-post.tsx`),
context: {
slug: node.fields.slug
}
})
}
})
}
一方でgatsby-node.jsは上のgatsby-node/index.tsを引っ張ってくるコードに書き換える。
"use strict"
require("ts-node").register({
compilerOptions: {
module: "commonjs",
target: "esnext",
},
})
require("./src/__generated__/gatsby-types")
const {
createPages,
onCreateNode,
} = require("./gatsby-node/index")
exports.createPages = createPages
exports.onCreateNode = onCreateNode
この時点で一度 gatsby develop で動くか確認すると一応、トランスパイルはうまくいっていると思う(エラーなどあれば any などでごまかしましょうw)
strictモードで手直ししていく
{
"compilerOptions": {
"target": "esnext", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["dom", "es2017"], /* Specify library files to be included in the compilation. */
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
"outDir": "./build", /* Redirect output structure to the directory. */
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"baseUrl": "src", /* Base directory to resolve non-absolute module names. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
"strictNullChecks": true, /* Enable strict null checks. */
"strictFunctionTypes": true, /* Enable strict checking of function types. */
"strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
},
"include": ["src/**/*", "gatsby-node/index.ts"],
"exclude": ["node_modules", "public", "build", "src/templates/blog-post.tsx"],
}
するとすぐにエラーが生じるので手直ししていきます。
生成されたundefined のところでエラーが生じるので修正する
gatsby-plugin-typegenで生成された型だと、Objectに'undefined'の可能性があるため型を拡張するかもしくは ? を挿入、もしくは if文 で 'undefined' の怒られを回避していく必要があります。
コンポーネントとpagesの手直して見た
gitはこちら - チュートリアルをTS化したブランチ(完成形)はココ
/** @jsx jsx */
import { FC } from "react"
import { jsx, css } from "@emotion/react"
import { useStaticQuery, Link, graphql } from "gatsby"
import { rhythm } from "../utils/typography"
const Layout: FC = ({ children }) => {
const data = useStaticQuery<GatsbyTypes.LayoutSiteMetadataQuery>(
graphql`
query LayoutSiteMetadata {
site {
siteMetadata {
title
}
}
}
`
)
return (
<div
css={css`
margin: 0 auto;
max-width: 700px;
padding: ${rhythm(2)};
padding-top: ${rhythm(1.5)};
`}
>
<Link to={`/`}>
<h3
css={css`
margin-bottom: ${rhythm(2)};
display: inline-block;
font-style: normal;
`}
>
{data.site?.siteMetadata?.title}
</h3>
</Link>
<Link
to={`/about/`}
css={css`
float: right;
`}
>
About
</Link>
{children}
</div>
)
}
export default Layout
import React from "react"
// import PropTypes from "prop-types"
import { Helmet } from "react-helmet"
import { useStaticQuery, graphql } from "gatsby"
interface SEOTypes {
description?: string,
lang?: string,
meta?: any,
title: string,
}
const SEO = ({
description,
lang,
meta,
title
}: SEOTypes) => {
const { site } = useStaticQuery<GatsbyTypes.SEOsiteMetadataQuery>(
graphql`
query SEOsiteMetadata {
site {
siteMetadata {
title
description
author
}
}
}
`
)
const metaDescription = description || site?.siteMetadata?.description
if(!lang) lang = 'ja';
if(!meta) meta = {};
return (
<Helmet
htmlAttributes={{
lang,
}}
title={title}
titleTemplate={`%s | ${site?.siteMetadata?.title}`}
meta={[
{
name: `description`,
content: metaDescription,
},
{
property: `og:title`,
content: title,
},
{
property: `og:description`,
content: metaDescription,
},
{
property: `og:type`,
content: `website`,
},
{
name: `twitter:card`,
content: `summary`,
},
{
name: `twitter:creator`,
content: site?.siteMetadata?.author,
},
{
name: `twitter:title`,
content: title,
},
{
name: `twitter:description`,
content: metaDescription,
},
].concat(meta)}
/>
)
}
export default SEO
/** @jsx jsx */
import {FC} from "react"
import { jsx, css } from '@emotion/react'
import { Link, graphql, PageProps } from "gatsby"
import { rhythm } from "../utils/typography"
import Layout from "../components/layout"
const Home: FC<PageProps<GatsbyTypes.MarkdownOfIndexQuery>> = ({data}) => {
return (
<Layout>
<div>
<h1
css={css`
display: inline-block;
border-bottom: 1px solid;
`}
>
Amazing Pandas Eating Things
</h1>
<h4>{data.allMarkdownRemark.totalCount} Posts</h4>
{data.allMarkdownRemark.edges.map(({ node }) => (
<div key={node.id}>
<Link
to={node.fields?.slug || '/'}
css={css`
text-decoration: none;
color: inherit;
`}
>
<h3
css={css`
margin-bottom: ${rhythm(1 / 4)};
`}
>
{node.frontmatter?.title}{" "}
<span
css={css`
color: #bbb;
`}
>
— {node.frontmatter?.date}
</span>
</h3>
<p>{node.excerpt}</p>
</Link>
</div>
))}
</div>
</Layout>
)
}
export const query = graphql`
query MarkdownOfIndex {
allMarkdownRemark {
totalCount
edges {
node {
id
frontmatter {
title
date(formatString: "DD MMMM, YYYY")
}
fields {
slug
}
excerpt
}
}
}
}
`
export default Home
import React, {FC} from "react"
import { graphql, PageProps } from "gatsby"
import Layout from "../components/layout"
const About: FC<PageProps<GatsbyTypes.AboutsiteMetadataQuery>> = ({ data }) => {
return (
<Layout>
<h1>About {data.site?.siteMetadata?.title}</h1>
<p>
We are the only site running on your computer dedicated to showing the
best photos and videos of pandas eating lots of food.
</p>
</Layout>
)
}
export const query = graphql`
query AboutsiteMetadata {
site {
siteMetadata {
title
}
}
}
`
export default About
import React, {FC} from "react"
import { graphql, PageProps } from "gatsby"
import Layout from "../components/layout"
const MyFiles: FC<PageProps<GatsbyTypes.myFilesAllFileQuery>> = ({data}) => {
return (
<Layout>
<div>
<h1>My Sites Files</h1>
<table>
<thead>
<tr>
<th>relativePath</th>
<th>prettySize</th>
<th>extension</th>
<th>birthTime</th>
</tr>
</thead>
<tbody>
{data.allFile.edges.map(({ node }, index) => (
<tr key={index}>
<td>{node.relativePath}</td>
<td>{node.prettySize}</td>
<td>{node.extension}</td>
<td>{node.birthTime}</td>
</tr>
))}
</tbody>
</table>
</div>
</Layout>
)
}
export const query = graphql`
query myFilesAllFile {
allFile {
edges {
node {
relativePath
prettySize
extension
birthTime(fromNow: true)
}
}
}
}
`
export default MyFiles
import React, {FC} from "react"
import { graphql, PageProps } from 'gatsby'
import Layout from "../components/layout"
import SEO from "../components/seo"
const BlogPost: FC<PageProps<GatsbyTypes.blogPostRemarkQuery>> = ({data}) => {
const post = data.markdownRemark
return (
<Layout>
<SEO title={post?.frontmatter?.title || "undefined"} description={post?.excerpt || "undefined"} />
<div>
<h1>{post?.frontmatter?.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post?.html || "undefined"}}/>
</div>
</Layout>
)
}
export const query = graphql`
query blogPostRemark($slug: String!) {
markdownRemark(fields: { slug: { eq: $slug } }) {
html
frontmatter {
title
}
excerpt
}
}
`
export default BlogPost
足りないライブラリを追加する
yarn add -D @types/react-helmet @types/typography
ここで、悲しいことにGatsbyJSのチュートリアルで使用されいてる"typography-theme-kirkham"の@types~が探しても見つからなかったのでTS化できなかった。
developするとエラーが起こる
ここで、gatsby developで動作確認したいところだが、理由は分からないですが "gatsby-plugin-typegen"のプラグインに入ったままだと "コンポーネント側で生成されたGraphQLの型が消えてなくなり、永遠にターミナル上でReloadされ続けて動かせなくなってしまいました。"
(理由は分からないのでわかる方がいたら教えて欲しいです。developの処理の時だけuseStaticQueryのGraphQLを認識せずに型が再生成されるから?なのかも。)
消えてしまったコンポーネントのGraphQLの型は gatsby build で再ビルドすれば元に戻ります。
そのため、gatsby develop で動作確認する際には gatsby-config.jsで設定した gatsby-plugin-typegen をコメントアウトする必要がありました。
ビルドしてserveしてみた
gatsby build
gatsby serve
http://localhost:9000/ で立ち上げることができれば完成。