Gatsby-starter-blogをTypeScript化する [2022-12版]

# はじめに

先月個人でブログを作成した際に、Gatsby.jsのgatsby-starter-blogというテンプレートを使ったのですが、デフォルトではTypeScript化されてないのでその手順を紹介しようと思います。

ネット上の関連記事ではconfigファイルのts化で一工夫されているものがいくつかあったのですが、今ではconfigファイルのts化が公式サポートされ、より簡単にTS移行できるようになっており本記事ではそこを踏まえて紹介させていただきます。

コードは動作確認後、筆者のレポジトリにあげてあるのでもし動かない！ということがあれば参考にしてみてください
[https://github.com/ironkicka/gatsby-starter-blog-typescript](https://github.com/ironkicka/gatsby-starter-blog-typescript)

# ****gatsby-starter-blogって？****

Gatsby.jsが提供しているStarterと呼ばれるテンプレートの１つです。

> [Starters](https://www.gatsbyjs.com/docs/starters/) are boilerplate Gatsby sites maintained officially, or by the community.****
> 

[Working with Starters | Gatsby](https://www.gatsbyjs.com/docs/how-to/local-development/starters/#reach-skip-nav)

Starterは****gatsby-starter-blog****以外にも多くの種類があり、以下のページでデモサイトを見ることができます

[Starters Library | Gatsby](https://www.gatsbyjs.com/starters/?v=2)

****gatsby-starter-blogの特徴としては、以下があります****

- 個人ブログ向けのレイアウト
かなりシンプルなレイアウトになってます
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/64b2aad2-2f90-0ab0-ce37-8e462a607f11.png)

    
- コンテンツはMarkdownファイルで記述し、それをHTML化して利用する
自動生成される雛形は以下のような構造をしており、基本的には`content/blog/`配下に記事ごとにディレクトリを作成し、その中に記事本体や埋め込む画像を配置します.この記事ディレクトリの名称がそのままページのパスになります。
例. new-beginningsコンテンツのページURL: `http://localhost:8000/new-beginnings/`
    
    ```
    .
    ├── LICENSE
    ├── README.md
    ├── content
    │   └── blog
    │       ├── hello-world
    │       │   ├── index.md
    │       │   └── salty_egg.jpg
    │       ├── my-second-post
    │       │   └── index.md
    │       └── new-beginnings
    │           └── index.md
    ├── gatsby-browser.js
    ├── gatsby-config.js
    ├── gatsby-node.js
    ├── gatsby-ssr.js
    ├── package-lock.json
    ├── package.json
    ├── public
    ├── src
    │   ├── components
    │   │   ├── bio.js
    │   │   ├── layout.js
    │   │   └── seo.js
    │   ├── images
    │   │   ├── gatsby-icon.png
    │   │   └── profile-pic.png
    │   ├── normalize.css
    │   ├── pages
    │   │   ├── 404.js
    │   │   ├── index.js
    │   │   └── using-typescript.tsx
    │   ├── style.css
    │   └── templates
    │       └── blog-post.js
    └── static
        ├── favicon.ico
        └── robots.txt
    ```
    

自分は普段からマークダウンで色々なメモを取ることが多いのでこの`gatsbyjs/gatsby-starter-blog`を使うことにしました!同じようにマークダウンコンテンツをそのまま記事にしたい方にはおすすめです！

# 手順

## スターターをもとに雛形を作成する

下記コマンドを実行して雛形を作成して下さい。

```bash
npx gatsby new myBlog https://github.com/gatsbyjs/gatsby-starter-blog
```

## Configファイルをts化する

### gatsby-config.js

下記２つの変更を行ってください

- 拡張子をtsにする
- エクスポートの記述をESモジュール形式にする(これ自体は行わなくてもエラーは出ないです)
    
    変更前
    
    ```jsx
    module.exports = {
      siteMetadata: {
        title: `Gatsby Starter Blog`,
        author: {
          name: `Kyle Mathews`,
          summary: `who lives and works in San Francisco building useful things.`,
        },
        description: `A starter blog demonstrating what Gatsby can do.`,
        siteUrl: `https://gatsbystarterblogsource.gatsbyjs.io/`,
        social: {
          twitter: `kylemathews`,
        },
      },
      ...
    }
    ```
    

変更後

```tsx
import type { GatsbyConfig } from "gatsby"

const config: GatsbyConfig = {
  siteMetadata: {
    title: `Gatsby Starter Blog`,
    author: {
      name: `Kyle Mathews`,
      summary: `who lives and works in San Francisco building useful things.`,
    },
    description: `A starter blog demonstrating what Gatsby can do.`,
    siteUrl: `https://gatsbystarterblogsource.gatsbyjs.io/`,
    social: {
      twitter: `kylemathews`,
    },
  },
  ...
}

export default config
```

この時点では、graphQLのクエリ結果に型を設定していないのでエラーが出ていますが、ビルド自体は成功するはずです。GraphQLへの型付けは後述する[こちら](https://qiita.com/iron-samurai/items/7ad1788a80fb8858469e#graphql%E3%81%AE%E7%B5%90%E6%9E%9C%E3%81%AB%E5%9E%8B%E3%82%92%E4%BB%98%E3%81%91%E3%82%8B)で説明しています。

### gatsby-node.js

- tsconfig.jsを作成する
CommonJSのモジュールであるpathをimportできるようにtsconfigを作成し、`esModuleInterop`をtrueにします。ただし、デフォルトでtrueなので以下のコマンドでtsconfig.jsonを作るだけで大丈夫です。
    
    ```bash
    npx tsc -init
    ```
    
- 拡張子をtsにする
- エクスポートの記述をESモジュール形式にする(これ自体は行わなくてもエラーは出ないです)

変更前

```tsx
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

...

exports.createPages = async ({ graphql, actions, reporter }) => {
...
}

exports.onCreateNode = ({ node, actions, getNode }) => {
...
}

exports.createSchemaCustomization = ({ actions }) => {
...
}
```

変更後

```tsx
import { Actions, GatsbyNode } from "gatsby"
import path from "path"
import { createFilePath } from "gatsby-source-filesystem"

export const createPages: GatsbyNode["createPages"] = async ({ graphql, actions, reporter }) => {
...
}

export const onCreateNode: GatsbyNode["onCreateNode"] = ({ node, actions, getNode }) => {
...
}

export const createSchemaCustomization: GatsbyNode["createSchemaCustomization"] = async ({ actions }: { actions: Actions}) => {
...
}
```

gatsby-config.tsと同様に、この時点ではgraphQLのクエリ結果に型を設定していないのでエラーが出ていますが、ビルド自体は成功するはずです。GraphQLへの型付けは後述する[こちら](https://qiita.com/iron-samurai/items/7ad1788a80fb8858469e#graphql%E3%81%AE%E7%B5%90%E6%9E%9C%E3%81%AB%E5%9E%8B%E3%82%92%E4%BB%98%E3%81%91%E3%82%8B)で説明しています。

### gatsby-broweser.js

拡張子をtsxにします。

デフォルトではtsで問題ないのですが、ここでは`wrapRootElement`などJSX.Elementを返す関数を exportすることもあるのでその際にエラーが出ないようにtsxにしておきます.

今回は使いませんが例えばRecoilを使うときは以下のように使ったりします

```tsx
export const wrapRootElement = ({element,props}:any)=>{
  return <RecoilRoot {...props} >{element}</RecoilRoot>
}
```

### gatsby-ssr.js

- 拡張子をtsにする
- エクスポートの記述をESモジュール形式にする(これ自体は行わなくてもエラーは出ないです)

変更前

```tsx
exports.onRenderBody = ({ setHtmlAttributes }) => {
  setHtmlAttributes({ lang: `en` })
}
```

変更後

```tsx
export const onRenderBody = ({ setHtmlAttributes }:any) => {
  setHtmlAttributes({ lang: `en` })
}
```

## GraphQLの結果に型を付ける

1. プラグインをインストール
gatsby-plugin-typegenというプラグインが用意されているのでインストールします
    
    ```bash
    npm i gatsby-plugin-typegen
    ```
    
    [gatsby-plugin-typegen | Gatsby](https://www.gatsbyjs.com/plugins/gatsby-plugin-typegen/)
    
2. gatsby-config.tsに以下を追加します
    
    ```tsx
    const config: GatsbyConfig = {
    ...
    {
          resolve: "gatsby-plugin-typegen",
          options: {
            emitSchema: {
              "src/__generated__/gatsby-schema.graphql": true,
              "src/__generated__/gatsby-introspection.json": true
            },
            emitPluginDocuments: {
              "src/__generated__/gatsby-plugin-documents.graphql": true
            }
          }
        },
      ],
    }
    ```
    
    追加後に`gatsby develop`をし直してください
    次のようなログが出て、ファイルが作成されていれば成功です。
    
    ```tsx
    success [typegen] emit schema into src/__generated__/gatsby-schema.graphql - 0.462s
    success [typegen] emit 3rd-party documents into src/__generated__/gatsby-plugin-documents.graphql - 0.276s
    success [typegen] generate type definitions to src/__generated__/gatsby-types.d.ts. (language: TypeScript) - 0.278s
    success [typegen] emit schema into src/__generated__/gatsby-introspection.json - 0.366s
    ```
    
    出力ファイル
    
    ```bash
    __generated__
    ├── gatsby-introspection.json
    ├── gatsby-plugin-documents.graphql
    ├── gatsby-schema.graphql
    └── gatsby-types.d.ts
    ```
    
3. 型を適用していく
    1. gatsby-node.ts
    変更前
        
        ```tsx
        const result = await graphql(`
            {
              allMarkdownRemark(sort: { frontmatter: { date: ASC } }, limit: 1000) {
                nodes {
                  id
                  fields {
                    slug
                  }
                }
              }
            }
          `)
        
        const posts = result.data.allMarkdownRemark.nodes
        
        // Create blog posts pages
          // But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js)
          // `context` is available in the template as a prop and as a variable in GraphQL
        
          if (posts.length > 0) {
            posts.forEach((post, index) => {
              const previousPostId = index === 0 ? null : posts[index - 1].id
              const nextPostId = index === posts.length - 1 ? null : posts[index + 1].id
        
              createPage({
                path: post.fields.slug,
                component: blogPost,
                context: {
                  id: post.id,
                  previousPostId,
                  nextPostId,
                },
              })
            })
          }
        ```
        
        変更後
        
        ```tsx
        const result = await graphql<{ allMarkdownRemark: GatsbyTypes.Query["allMarkdownRemark"]}>(`
            {
              allMarkdownRemark(sort: { frontmatter: { date: ASC } }, limit: 1000) {
                nodes {
                  id
                  fields {
                    slug
                  }
                }
              }
            }
          `)
        
        const posts = result.data?.allMarkdownRemark.nodes
        
        // Create blog posts pages
          // But only if there's at least one markdown file found at "content/blog" (defined in gatsby-config.js)
          // `context` is available in the template as a prop and as a variable in GraphQL
          if(!posts) return;
          if (posts.length > 0) {
            posts.forEach((post, index) => {
              const previousPostId = index === 0 ? null : posts[index - 1].id
              const nextPostId = index === posts.length - 1 ? null : posts[index + 1].id
        
              createPage({
                path: post.fields?.slug!,
                component: blogPost,
                context: {
                  id: post.id,
                  previousPostId,
                  nextPostId,
                },
              })
            })
          }
        ```
        
    2. gatsby-config.ts
    変更前
        
        ```tsx
        {
              resolve: `gatsby-plugin-feed`,
        			options: {
        			...
        				feeds: [
                  {
                    serialize: ({ query: { site, allMarkdownRemark }}) => {
                      return allMarkdownRemark.nodes.map(node => {
                        return Object.assign({}, node.frontmatter, {
                          description: node.excerpt,
                          date: node.frontmatter.date,
                          url: site.siteMetadata.siteUrl + node.fields.slug,
                          guid: site.siteMetadata.siteUrl + node.fields.slug,
                          custom_elements: [{ "content:encoded": node.html }],
                        })
                      })
                    },
        					...
        				]
        			}
        			
        }
        ```
        
        変更後
        
        ```tsx
        {
              resolve: `gatsby-plugin-feed`,
        			options: {
        			...
        				feeds: [
                  {
                    serialize: ({ query: { site, allMarkdownRemark } }:{query:{site:GatsbyTypes.Query['site'],allMarkdownRemark:GatsbyTypes.Query["allMarkdownRemark"]}}) => {
                      return allMarkdownRemark.nodes.map(node => {
                        return Object.assign({}, node.frontmatter, {
                          description: node.excerpt,
                          date: node.frontmatter?.date,
                          url: site?.siteMetadata?.siteUrl! + node.fields?.slug,
                          guid: site?.siteMetadata?.siteUrl! + node.fields?.slug,
                          custom_elements: [{ "content:encoded": node.html }],
                        })
                      })
                    },
        					...
        				]
        			}
        			
        }
        ```
        

これでconfigファイル内のGraphQL周りのエラーは解消されたはずです！

## コンポーネントをtsxにする

次にページの表示に使っている種々のコンポーネントをTS化していきます。

### src/pages配下のコンポーネントをtsxにする

1. 拡張子の変更
    
    まず以下のようにusing-typescript.tsx削除し、残ったファイルの拡張子をtsxに変更します
    
    変更前のsrc/pages
    
    ```
    .
    ├── 404.js ←拡張子をtsxに変更
    ├── index.js ←拡張子をtsxに変更
    └── using-typescript.tsx ←削除
    ```
    
    変更後
    
    ```
    .
    ├── 404.tsx
    └── index.tsx
    ```
    
    この時点で、[http://localhost:8000/](http://localhost:8000/) にアクセスして以下が表示されていれば成功です.
    反映されない場合は、gatsby-developをし直すか、.cacheを消してみてください
    
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/74e4c73f-c9d9-b8c5-de37-7c420b2f1581.png)

    
2. index.tsxの型エラーを解消する
    
    現時点では、propsとpostsをmapする部分でエラーが出ているのでそれを解消します。
    
    以下のようにpropsに型を追加します
    変更前
    
    ```tsx
    import { Link, graphql } from "gatsby"
    const BlogIndex = ({ data, location }) => {
    ...
    }
    ```
    
    変更後
    
    ```tsx
    import { Link, graphql, PageProps } from "gatsby"
    const BlogIndex = ({ data, location }:PageProps<GatsbyTypes.Query>) => {
    ...
    }
    ```
    
    posts周りでもエラーが出ますが詳細はレポジトリの[こちら](https://github.com/ironkicka/gatsby-starter-blog-typescript/blob/main/src/pages/index.tsx)をご覧ください
    
3. 404.tsxの型エラーを解消する
index.tsx同様にpropsに型を定義します
変更前
    
    ```tsx
    import { graphql } from "gatsby"
    const NotFoundPage = ({ data, location }) => {
    const siteTitle = data.site.siteMetadata.title
    ...
    }
    ```
    
    変更後
    
    ```tsx
    import { graphql, PageProps } from "gatsby"
    
    const NotFoundPage = ({ data, location }:PageProps<GatsbyTypes.Query>) => {
    const siteTitle = data.site?.siteMetadata?.title||''
    ...
    }
    ```
    

以上でsrc/pages配下のTS化は一旦終了です！

### components配下のコンポーネントをtsxにする

この時点でもページ自体は表示されますが、index.tsxや404.tsxでは以下のようなエラーが出ていると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/4f5d6cfb-2449-54a6-627b-2f4f361e9a4b.png)


これはSeoコンポーネントがtsx化されておらずpropsの型がないためです。なので、このセクションでSeoコンポーネント含む componentsディレクトリ配下のコンポーネントをtsx化します

まずは準備としてtsconfig.jsonの`compilerOptions`に以下を追加します

```tsx
"jsx": "react"
```

これをしないと以下のようなエラーが出て怒られます。

```bash
Cannot use JSX unless the '--jsx' flag is provided
```

それでは各コンポーネントをtsx化していきましょう。

- Seoコンポーネントをtsx化する
    1. ファイル拡張子を変更してseo.tsxにする
    2. 型をつける
    下記ように型を定義してあげます。これでindex.tsx,404.tsx内のエラーは消えるはずです。
        
        ```tsx
        type Props = Readonly<{
          description?:string;
          title:string;
          children?:React.ReactNode
        }>
        
        const Seo = ({ description, title, children }:Props) => {
        ...
        
        }
        ```
        
- Layoutコンポーネントをtsx化する
    1. ファイル拡張子を変更してlayout.tsxにする
    2. 型をつける
        
        ```tsx
        import { WindowLocation } from "@reach/router"
        
        type Props = Readonly<{
          location: WindowLocation;
          title: string;
          children?: React.ReactNode
        }>
        
        export const Layout = ({ location, title, children }: Props) => {
        ...
        }
        ```
        
    3. Gatsbyグローバル変数の型定義ファイルを作成する
    ステップ2までの状態では以下のようにGatsbyのグローバル変数ではる’`__PATH_PREFIX__`が存在しない’とエラーが出ます
        
        ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366323/e3c0dbb8-27a1-20c8-8818-cfdd024c6f43.png)

        
        これを解消するため、global.d.tsファイルをsrc配下に置き、以下を記述します
        
        ```tsx
        declare const __PATH_PREFIX__: string
        ```
        
        以下を参考にしました
        
        > • Gatsby’s global variable like `__PATH_PREFIX__` can be handled by declaring this code somewhere:
        > 
        
        [gatsby-typescript | Gatsby](https://www.gatsbyjs.com/plugins/gatsby-typescript/)
        
- Bioコンポーネントをtsx化する
  Bioコンポーネントはpropsを受け取らないのでファイル拡張子をtsxに変更するだけで完了です。

### templates配下のblog-post.jsをtsxにする
最後に、記事の詳細ページ用のコンポーネントを記述している`blog-post.js`をtsx化します。
手順は以下２つです
1. ファイル拡張子をtsxにする
2. 型をつける
   ファイル内の`BlogPostTemplate`と`Head`の２つのコンポーネントに対して以下のように型をつけてあげます。この際、`BlogPostTemplate`では`string`を期待する箇所に`string|undefined`を渡しているためにエラーが起きる箇所がいくつかあるのですが、細かい話になるので詳しくはレポジトリの[こちら](https://github.com/ironkicka/gatsby-starter-blog-typescript/blob/main/src/templates/blog-post.tsx)をご覧ください。
   ```tsx
   const BlogPostTemplate = ({
     data: { previous, next, site, markdownRemark: post },
     location,
   }:PageProps<GatsbyTypes.Query&{previous:GatsbyTypes.Query['markdownRemark'],next:GatsbyTypes.Query['markdownRemark']}>) => {
   ...
   }

   export const Head = ({ data: { markdownRemark: post } 
   }:PageProps<GatsbyTypes.Query>) => {
     return (
       <Seo
         title={post?.frontmatter?.title||''}
         description={post?.frontmatter?.description || post?.excerpt||''}
       />
     )
   }
   ```
3. gatsby-node.tsでblog-post.jsを参照している部分を修正する
最後に`gatsby-node.ts`を参照します。以下のように参照するテンプレートのパスを指定する部分を`./src/templates/blog-post.js`から`./src/templates/blog-post.tsx`にして完了です！
   ```ts
   const blogPost = path.resolve(`./src/templates/blog-post.tsx`)
   ```

以上でgatsby-starter-blogのTS化は終了です。最終的には以下のような構造になっているかと思います。

```shell
.
├── LICENSE
├── README.md
├── content
│   └── blog
│       ├── hello-world
│       │   ├── index.md
│       │   └── salty_egg.jpg
│       ├── my-second-post
│       │   └── index.md
│       └── new-beginnings
│           └── index.md
├── gatsby-browser.tsx
├── gatsby-config.ts
├── gatsby-node.ts
├── gatsby-ssr.ts
├── package-lock.json
├── package.json
├── public
├── node_modules
├── src
│   ├── __generated__
│   │   ├── gatsby-introspection.json
│   │   ├── gatsby-plugin-documents.graphql
│   │   ├── gatsby-schema.graphql
│   │   └── gatsby-types.d.ts
│   ├── components
│   │   ├── bio.tsx
│   │   ├── layout.tsx
│   │   └── seo.tsx
│   ├── global.d.ts
│   ├── images
│   │   ├── gatsby-icon.png
│   │   └── profile-pic.png
│   ├── normalize.css
│   ├── pages
│   │   ├── 404.tsx
│   │   └── index.tsx
│   ├── style.css
│   └── templates
│       └── blog-post.tsx
├── static
│   ├── favicon.ico
│   └── robots.txt
└── tsconfig.json
```
# Tips
TS化する過程で、加えた変更は正しいはずのに`gatsby develop`するとエラーになる/うまく反映されない場合は、`.cache`ディレクトリを削除してみてください。これをしないとts/tsxファイルで作り直しても、`gatsby`が変更前の状態を参照してしまうことがあるためです。

# おわりに

いかがでしたでしょうか。ほぼ作業だけで特に難しいことはしなくてよかったかと思います。皆さんの個人ブログ作成の助けになれば幸いです。ちなみに自分はデザイン・機能ともにいい感じにならずなかなか公開できずにいます。。笑。

# 参考
- [Gatsby の Config ファイルがいつの間にか TypeScript 対応していた](https://www.coppla-note.net/posts/gatsby/convert-typescript/)
- [Gatsby.jsのTypeScript化 2020](https://zenn.dev/ryo_kawamata/articles/gatsby-ts-2020)
- [Gatsby.js を完全TypeScript化する](https://qiita.com/Takepepe/items/144209f860fbe4d5e9bb)