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 を無効にしています。

まとめ

以上で終わりです!

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

それではまた。


Homeへ戻る
profile picture

Kosuke Kihara

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

Kohsuk11KOHSUKkohsuk.tech@gmail.com