GatsbyをTypeScript化してESLintとPrettierを導入する

2020.12.05

💻
TECH

GatsbyJSとTypeScriptでブログを作成して公開する(3)

~ GatsbyをTypeScript化してESLintとPrettierを導入する ~


今回は、これまでにgatsby-starter-blogを用いて生成されたソースをTypeScriptで書けるように変更していきます。

環境構築についてはこちらを、gatsby-starter-blogについてはこちらをご覧いただければと思います。


変更履歴

  • 2020/12/12: .eslintrcの設定内容を変更
  • 2021/09/19: スタイルの調整

参考にさせていただいた記事

こちらのZENNの投稿がとても参考になりました。


検証環境

  • node 14.15.1
  • yarn 1.22.3
  • gatsby cli 2.14.0
  • TypeScript 4.0.3

新しいプロジェクトの作成

まずgatsby-starter-blogを使った新しいGatsbyのプロジェクトを作成します。

以下のコマンドを叩きます。

    # gatsby-typescriptという名前でプロジェクト作成
    $ gatsby new gatsby-typescript https://github.com/gatsbyjs/gatsby-starter-blog 

    # ディレクトリを変更
    $ cd gatsby-typescript

    # development サーバーの立ち上げ
    $ gatsby develop

この状態でhttp://localhost:8000にアクセスすると、以下のようなページを確認できます。

localhost:8000


ページ・コンポーネントのTypeScript化

GatsbyJSはネイティブでTypeScriptをサポートしているので、

拡張子を.ts もしくは.tsxにするだけで、TypeScriptでページやコンポーネントを記述することができます。

また、gatsby-starter-blogにはすでに

src\pages\using-typescript.tsxというTypeScriptのページが含まれています。

src\pages\using-typescript.tsx

// If you don't want to use TypeScript you can delete this file!
import React from "react"
import { PageProps, Link, graphql } from "gatsby"

import Layout from "../components/layout"
import SEO from "../components/seo"

type DataProps = {
  site: {
    buildTime: string
  }
}

const UsingTypescript: React.FC<PageProps<DataProps>> = ({
  data,
  path,
  location,
}) => (
  <Layout title="Using TypeScript" location={location}>
    <SEO title="Using TypeScript" />
    <h1>Gatsby supports TypeScript by default!</h1>
    <p>
      This means that you can create and write <em>.ts/.tsx</em> files for your
      pages, components etc. Please note that the <em>gatsby-*.js</em> files
      (like gatsby-node.js) currently don't support TypeScript yet.
    </p>
    <p>
      For type checking you'll want to install <em>typescript</em> via npm and
      run <em>tsc --init</em> to create a <em>.tsconfig</em> file.
    </p>
    <p>
      You're currently on the page "{path}" which was built on{" "}
      {data.site.buildTime}.
    </p>
    <p>
      To learn more, head over to our{" "}
      <a href="https://www.gatsbyjs.com/docs/typescript/">
        documentation about TypeScript
      </a>
      .
    </p>
    <Link to="/">Go back to the homepage</Link>
  </Layout>
)

export default UsingTypescript

export const query = graphql`
  {
    site {
      buildTime(formatString: "YYYY-MM-DD hh:mm a z")
    }
  }
`

こちらのページはhttp://localhost:8000/using-typescriptで確認できます。

このようにページやコンポーネントをTypeScriptで書くだけなら、

特に設定を行う必要はありません。


開発環境としてのTypeScript

設定なしでTypeScriptが使えるのはわかりましたが、

これでは、TypeScriptを使う利点があまり得られません。

using-typescript.tsxにも書いてある通り、

タイプチェックを行うためには、typescriptのインストールとtsc --initを実行して.tsconfigファイルを作成する必要があります。

これだけでも良いのですが、せっかくならLinterとかFormatterとかもカスタマイズしたいところです。

というわけで、ここではTypeScriptに加えてESLINTPrettierの導入までおこない、

TypeScriptを利用した開発環境として整えたいと思います。

また、今回はVSCodeを使用することを前提として進めます。


TypeScriptのインストール

インストール

後にインストールするESLintのプラグインの依存関係となるため、

typescriptもインストールします。

$ yarn add -D typescript

tsconfig.jsonの生成

次のコマンドを実行して、tsconfig.jsonを生成します。

yarn run tsc --init

設定内容自体は以下のように変更してください。

tsconfig.json

{
  "target": "esnext",
  "module": "esnext",
  "lib": ["DOM", "ESNext"],
  "jsx": "react",
  "noEmit": true,
  "strict": true,
  "moduleResolution": "node",
  "esModuleInterop": true,
  "skipLibCheck": true,
  "forceConsistentCasingInFileNames": true
}

GatsbyJS にESLint を導入する

ESLint のセットアップを行っていきます。


ESLintのインストール

まず、eslinteslint-loaderをインストールします。

$ yarn add -D eslint eslint-loader

ESLintの設定ファイルの作成

ESLintの設定ファイルを作成します。

以下のコマンドを実行してください。

$ yarn run eslint --init

このコマンドを実行すると、いくつか質問が出てくるのでそれに答えていくことで、

設定ファイルを生成してくれます。

今回は以下のように答えていきます。

? How would you like to use ESLint? (Use arrow keys)
  To check syntax only
> To check syntax and find problems
  To check syntax, find problems, and enforce code style

? What type of modules does your project use? (Use arrow keys)       
> JavaScript modules (import/export)
  CommonJS (require/exports)
  None of these

? Which framework does your project use? (Use arrow keys)
> React
  Vue.js
  None of these

? Does your project use TypeScript? (y/N) y

? Where does your code run? (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Browser
 ( ) Node

? What format do you want your config file to be in?
  JavaScript
  YAML
> JSON

The config that you've selected requires the following dependencies:

eslint-plugin-react@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
? Would you like to install them now with npm? (Y/n) y

質問の最後に、今回の設定に必要なパッケージをインストールするか聞かれますので、これもy(es)で答えてインストールしてもらいます。

処理がすべて終わると.eslintrc.jsonというファイルが作成されます。

.eslintrc.json

{
    "env": {
        "browser": true,
        "es6": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/eslint-recommended"
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
}

ESLintのルールセット・プラグインの導入

次にESLintのルールセット・プラグインを導入します。

ここでは、Airbnbが公開しているeslint-config-airbnbを導入してみます。

eslint-config-airbnbとその依存関係をインストールします。

$ yarn add -D eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks

typescript-eslintをインストールします。

$ yarn add -D @typescript-eslint/eslint-plugin @typescript-eslint/parser

gatsby-plugin-eslintのインストール

gatsby-plugin-eslintというGatsbyのプラグインをインストールします。

$ yarn add -D gatsby-plugin-eslint

yarn develop時に検査が行われるよう、以下をgatsby-config.jsに追加します。

gatsby-config.js

module.exports = {
  plugins: [
    /*

      ...省略

    */
    {
      resolve: 'gatsby-plugin-eslint',
      options: {
        test: /\.js$|\.jsx$|\.ts$|\.tsx$/,
        exclude: /(node_modules|.cache|public)/,
        stages: ['develop'],
        options: {
          emitWarning: true,
          failOnError: false
        }
      }
    }
  ]
}

Prettierの導入

Prettier本体

gatsby-starter-blogにはPrettierもあらかじめ含まれているので、

インストールは必要ありません。

もしインストールが必要ならyarn add -D prettierでインストール可能です。


Prettierの設定

次にPrettierの設定ファイルです。

ルートディレクトリに.prettierrcというファイルがありますので、

そちらを編集すればOKです!

例えば以下のように設定してみます。

.prettierrc

{
  "arrowParens": "avoid",
  "semi": true,
  "endOfLine": "lf",
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5"
}

このあたりのルールはプロジェクトによりけりでしょうから、

こちらを見て必要なものを設定していってください。


.prettierignoreの編集

Prettierによるフォーマットを避けたいファイルやディレクトリは.prettierignoreに設定します。

すでに用意されているファイルですが、以下は追加しておくとよいでしょう

tsconfig.json
.eslintrc.json

ESLintとPrettierの競合を解決する

今回LinterとしてESLintをCode FormatterとしてPrettierを用いるわけですが、

そのままだと、両者のルールは競合してしまいます。

これらはいちいち設定を探して回避もできますが、競合ルールを簡単に解決する方法があるのでこちらを利用します。


eslint-config-prettierをインストールします。

$ yarn add -D eslint-config-prettier

ESLintの設定を編集

ESLint用のプラグインとPrettierをインストールしたら

ESLintの設定を編集します。

.eslintrc.jsonファイルを開いて、以下のように編集してください。

{
  /*

    ...省略

  */
  "extends": [
    "airbnb",
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "prettier",
    "prettier/@typescript-eslint",
    "prettier/react
  ],
  "parser": "@typescript-eslint/parser",
  "settings": {
    "import/resolver": {
      "node": {
        "extensions": [
          ".js",
          ".jsx",
          ".ts",
          ".tsx"
        ]
      }
    }
  },

  /*

    ...省略

  */

  "rules": {
    "no-use-before-define": "off",
    "quotes": [2, "single", { "avoidEscape": true }],
    "react/jsx-filename-extension": [
      "error",
      { "extensions": [".jsx", ".tsx"] }
    ],
    "react/prop-types": "off",
    "@typescript-eslint/explicit-module-boundary-types": "off",
    "@typescript-eslint/no-var-requires": "off",
    "react/no-danger": "off",
    "react/no-unescaped-entities": "off",
    "import/no-extraneous-dependencies": "off",
    "import/extensions": [
      "error",
      "ignorePackages",
      {
        "js": "never",
        "jsx": "never",
        "ts": "never",
        "tsx": "never"
      }
    ]
  }
}

VSCodeのESLint Prettierの拡張機能を利用する

次にVSCodeにESLintとPrettierの拡張機能をインストールします。

これらを入れることで、リアルタイムのLintingと、

ファイル保存時のFormatを可能にします。


インストール

VSCodeの拡張機能の検索でそれぞれすぐに見つかると思いますのでインストールしてください。


VSCodeの設定

VSCodeをひらき、Ctrl + Shift + P(MacはCmd + Shift + P)でコマンドパレットを表示し、

workspace settings jsonと入力してEnterを押してください。

setting.jsonファイルが開きますので、これに以下を記述して保存して下さい。

{
  "editor.tabSize": 2,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "files.eol": "\n"
}

ESLintとgatsby-plugin-eslintの確認

ここで一度設定の確認をします。

まず、起動している場合は一度開発サーバーを止め、

gatsby developを実行しなおします。

(うまく実行できない場合は一度npm installを実行してみてください)

すると以下のようなエラーが大量に表示されるはずです。

warning ESLintError:
C:\Users\Kohsuk\Documents\Projects\gatsby\gatsby-typescript\gatsby-browser.js
   2:8  error  Strings must use singlequote  quotes    
   3:8  error  Strings must use singlequote  quotes    
   5:8  error  Strings must use singlequote  quotes    
   7:8  error  Strings must use singlequote  quotes    
  10:8  error  Strings must use singlequote  quotes    

✖ 5 problems (5 errors, 0 warnings)
  5 errors and 0 warnings potentially fixable with the 
`--fix` option.

gatasby-starter-blogのコードが今回設定したESLintのルールに違反しているためです。

ESLintのエラーが出ていてもhttp://localhost:8000でページの確認自体は可能です。


エラーを解消する

以上でESLint, Prettier周りの設定は完了しました。

今度はエラーを修正していきます。


フォーマット用のスクリプトを作成

まず、フォーマットのためのスクリプトを用意します。

package.jsonにデフォルトでformatが用意されており、prettierでフォーマットできるようになっています。

ここでESLintも実行できるようにしておき、ESLintのエラーも一緒に解消したいと思います。

  "scripts": {
    "format:prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\"",
    "format:eslint": "eslint --fix \"**/*.{js,jsx,ts,tsx}\"",
    "format": "yarn format:eslint && yarn format:prettier",
  }

今回は、ESLint -> Prettier の順で実行するスクリプトを書きました。

eslint-plugin-prettierというプラグインを用いれば、

ESlint内でprettierを実行することもできますが、

こちらの記事を参考にさせていただき、

今回はこのような方法を取っています。


formatスクリプトを実行

では上で設定したスクリプトを実行して、エラーを解消します。

$ yarn format

取り切れないエラー

スクリプトの実行でフォーマットがおこなわれるので、

大方のエラーは解消されるはずです。

ただ、VSCode上で見ると、ESLint Typescriptのエラーがいくつか取り切れていないかと思います。


1. src/components/layout.jsの5行目の__PATH_PREFIX__

.eslintrc.jsonに以下を追加すれば解決します。

`.eslintrc.json

{

  "globals": {
    "__PATH_PREFIX__": true
  }

}

2. .jsファイルにJsxがある

.jsxまたは.tsx以外にJsxを書くとエラーになるように設定してあります。

これらのファイルは後でTypeScriptに書き換え、.tsxに変更するので問題ないですが、気になる場合はいったん拡張子を.jsxに変更します。

注意点として、src/templates/blog-post.jsの拡張子を変更した場合は、このパスを参照しているgatsby-node.js8行目の拡張子もjsxに変更する必要があります。


3. src/components/seo.jsの9行目prop-types

TypeScriptに書き換える場合は、props-typesでタイプチェックする必要がないので、

無視しておいてかまわないかと思います。

JavaScriptのままにしておきたい場合は、エラーメッセージの通り、prop-typesをインストールしてください(yarn add prop-types)。


4. src/pages/using-typescript.tsxの5, 6行目

jsファイルからimportしているため、型定義ファイルがなくエラーになっています。

layout.js, seo.jsともにTypeScriptに書き換えれば問題ないので無視してかまわないと思います。


ここで再度gatsby developコマンドを実行すると、prop-typesのエラー以外は消えているはずです。


TypeScriptに書き換える

ようやく各ファイルのTypeScript化を進めていきます。

まず、src/pages/index.jsの拡張子を.tsxに変更してみます。

するとBlogIndexコンポーネントの引数に型の指定がないためエラーとなります。

まず以下のように変更します(変更というコメントのある行)。

src/pages/index.tsx

import React from 'react';
import { Link, graphql, PageProps } from 'gatsby'; // <- 変更

import Bio from '../components/bio';
import Layout from '../components/layout';
import SEO from '../components/seo';

const BlogIndex: React.FC<PageProps> = ({ data, location }) => { // <- 変更
  const siteTitle = data.site.siteMetadata?.title || 'Title'; // <- ここはエラーが残っている
  const posts = data.allMarkdownRemark.nodes; // <- ここはエラーが残っている
/*

  ...省略

*/
};

export default BlogIndex;

export const pageQuery = graphql`
  query BlogIndex { // <- 変更
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      nodes {
        excerpt
        fields {
          slug
        }
        frontmatter {
          date(formatString: "MMMM DD, YYYY")
          title
          description
        }
      }
    }
  }
`;

これで, Propsの型に関するエラーは解消されたはずです。

しかしまだ、ここはエラーが残っているとコメントを書いた部分にエラーが出ています。

こちらは、GraphQLのクエリのレスポンスを参照している部分で、

その定義がないためエラーとなっています。


GraphQLのレスポンスの型を生成する

GraphQLのレスポンスの型は自動生成することができます。

今回はgatsby-plugin-typegenというものを使用します。


gatsby-plugin-typegenをインストール

まずはプラグインをインストールします。

$ yarn add gatsby-plugin-typegen

つぎにこのプラグインを読み込めるようにgatsby-config.jsに追記します。

gatsby-config.js

module.exports = {
  /*
    ...省略
  */
  plugins: [
    /*
      ...省略
    */
    'gatsby-plugin-typegen',
  ]

ここまで出来たら、gatsby developを実行しなおします。

すると、src/__generated__/gatsby-types.tsが自動生成されます。

このファイルに以下のような定義が追加されているはずです。

src/__generated__/gatsby-types.ts

  type BlogIndexQuery = { readonly site: Maybe<{ readonly siteMetadata: Maybe<Pick<SiteSiteMetadata, 'title'>> }>, readonly allMarkdownRemark: { readonly nodes: ReadonlyArray<(
        Pick<MarkdownRemark, 'excerpt'>
        & { readonly fields: Maybe<Pick<Fields, 'slug'>>, readonly frontmatter: Maybe<Pick<Frontmatter, 'date' | 'title' | 'description'>> }
      )> } }

BlogIndexQueryという名前は、先ほどsrc/pages/index.tsxのクエリの定義部分に追加した

BlogIndexというクエリの名前にQueryがついたもので、

このルールで生成されるようです。


生成されたクエリの型を使用する

ではこれを使ってみます。

再びsrc/pages/index.tsxを開き以下のように編集してください。

const BlogIndex: React.FC<PageProps<GatsbyTypes.BlogIndexQuery>> = ({ // <- 変更
  data,
  location,
}) => {

このようにGatsbyTypesで生成された型を参照できます。

あとはオプショナルチェイニング演算子?.や三項演算子などを使えばエラーを解決できるはずです。

最終的にはこのようになりました。

// src/__generated__/gatsby-types.ts

import React from 'react';
import { Link, graphql, PageProps } from 'gatsby';

import Bio from '../components/bio';
import Layout from '../components/layout';
import SEO from '../components/seo';

const BlogIndex: React.FC<PageProps<GatsbyTypes.BlogIndexQuery>> = ({
  data,
  location,
}) => {
  const siteTitle = data.site?.siteMetadata?.title || 'Title';
  const posts = data.allMarkdownRemark.nodes;

  if (posts.length === 0) {
    return (
      そいstjjt location={location} title={siteTitle}>
        <SEO title="All posts" />
        <Bio />
        <p>
          No blog posts found. Add markdown posts to "content/blog" (or the
          directory you specified for the "gatsby-source-filesystem" plugin in
          gatsby-config.js).
        </p>
      </Layout>
    );
  }

  return (
    <Layout location={location} title={siteTitle}>
      <SEO title="All posts" />
      <Bio />
      <ol style={{ listStyle: 'none' }}>
        {posts.map(post => {
          const title = post.frontmatter?.title || post.fields?.slug;

          return (
            <li key={post.fields?.slug}>
              <article
                className="post-list-item"
                itemScope
                itemType="http://schema.org/Article"
              >
                <header>
                  <h2>
                    <Link to={post.fields?.slug || '/'} itemProp="url">
                      <span itemProp="headline">{title}</span>
                    </Link>
                  </h2>
                  <small>{post.frontmatter?.date}</small>
                </header>
                <section>
                  <p
                    dangerouslySetInnerHTML={{
                      __html:
                        post.frontmatter?.description || post.excerpt || '',
                    }}
                    itemProp="description"
                  />
                </section>
              </article>
            </li>
          );
        })}
      </ol>
    </Layout>
  );
};

export default BlogIndex;

export const pageQuery = graphql`
  query BlogIndex {
    site {
      siteMetadata {
        title
      }
    }
    allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
      nodes {
        excerpt
        fields {
          slug
        }
        frontmatter {
          date(formatString: "MMMM DD, YYYY")
          title
          description
        }
      }
    }
  }
`;

他のコンポーネントもこの方法でTypeScriptに置き換えていくことができます!


gatsby-node.jsのTypeScript化

あとはgatsby-node.jsをTypeScript化します。

まずはts-nodeをインストールします。

$ yarn add -D ts-node

次に、src/gatsby-node/index.tsファイルを作成し、gatsby-node.jsを丸ごとコピーして貼り付けます。

コンポーネントの時と同じように、エラーがなくなるまで修正します。

少々苦労しましたが、最終的にこれでエラーがなくなります。

src/gatsby-node/index.ts

/* eslint-disable @typescript-eslint/no-non-null-assertion */
import path from 'path';
import { createFilePath } from 'gatsby-source-filesystem';
import { GatsbyNode, Actions } from 'gatsby';

export const createPages: GatsbyNode['createPages'] = async ({
  graphql,
  actions,
  reporter,
}) => {
  const { createPage } = actions;

  // Define a template for blog post
  const blogPost = path.resolve('./src/templates/blog-post.jsx');

  // Get all markdown blog posts sorted by date
  const result = await graphql<{
    allMarkdownRemark: Pick<GatsbyTypes.Query['allMarkdownRemark'], 'nodes'>;
  }>(
    `
      {
        allMarkdownRemark(
          sort: { fields: [frontmatter___date], order: ASC }
          limit: 1000
        ) {
          nodes {
            id
            fields {
              slug
            }
          }
        }
      }
    `
  );

  if (result.errors) {
    reporter.panicOnBuild(
      'There was an error loading your blog posts',
      result.errors
    );
    return;
  }

  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 && 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,
        },
      });
    });
  }
};

export const onCreateNode: GatsbyNode['onCreateNode'] = ({
  node,
  actions,
  getNode,
}) => {
  const { createNodeField } = actions;

  if (node.internal.type === 'MarkdownRemark') {
    const value = createFilePath({ node, getNode });

    createNodeField({
      name: 'slug',
      node,
      value,
    });
  }
};

export const createSchemaCustomization: GatsbyNode['createSchemaCustomization'] = async ({
  actions,
}: {
  actions: Actions;
}) => {
  const { createTypes } = actions;

  // Explicitly define the siteMetadata {} object
  // This way those will always be defined even if removed from gatsby-config.js

  // Also explicitly define the Markdown frontmatter
  // This way the "MarkdownRemark" queries will return `null` even when no
  // blog posts are stored inside "content/blog" instead of returning an error
  createTypes(`
    type SiteSiteMetadata {
      author: Author
      siteUrl: String
      social: Social
    }

    type Author {
      name: String
      summary: String
    }

    type Social {
      twitter: String
    }

    type MarkdownRemark implements Node {
      frontmatter: Frontmatter
      fields: Fields
    }

    type Frontmatter {
      title: String
      description: String
      date: Date @dateformat
    }

    type Fields {
      slug: String
    }
  `);
};

次に、もともとあったgatsby-node.jsは上のファイルを参照するように変更します。

gatsby-node.js

/* eslint-disable */
'use strict';

require('ts-node').register({
  compilerOptions: {
    module: 'commonjs',
    target: 'esnext',
  },
});

require('./src/__generated__/gatsby-types');

const {
  createPages,
  onCreateNode,
  createSchemaCustomization,
} = require('./src/gatsby-node/index');

exports.createPages = createPages;
exports.onCreateNode = onCreateNode;
exports.createSchemaCustomization = createSchemaCustomization;

gatsby-node.jssrc/gatsby-node/index.tsではうまくエラーが取れない部分があったので、

一部ESLintを無効にしています。


まとめ

以上で終わりです!

かなり長くなってしまいましたが、参考になれば幸いです。

それではまた。


KOHSUK

I'm a Web Developer👑 who shares tips on Tech, Productivity, Design and MORE!

Code snippets licensed under MIT, unless otherwise noted.

Content & Graphics © 2022, KOHSUK