🐳

[React] ESLintを設定して秩序をもたらしてみた🪡[Typescript] [Nextjs]

2023/12/01に公開

この記事で紹介する eslint の設定ファイルはこちらに沿って作成しています!是非そちらも見ていただけると幸いです 🥣

https://zenn.dev/tara_is_ok/articles/05b3a6dc2ebdd7

TL;DR

huskylint-stagedの細かな設定は既にたくさんの方が記事にしているため割愛します 🧑‍🔬

パッケージのインストール

npm install -D eslint eslint-config-next @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-import eslint-plugin-react eslint-plugin-simple-import-sort
.eslintrc.js
module.exports = {
  extends: [
    "next/core-web-vitals",
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:react/recommended",
    "plugin:react-hooks/recommended",
    "plugin:react/jsx-runtime",
    "plugin:eslint-comments/recommended",
    "plugin:storybook/recommended",
    "prettier",
  ],
  env: { browser: true, node: true, es6: true },
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: "latest",
    sourceType: "module",
    project: "./tsconfig.json",
    tsconfigRootDir: __dirname,
  },
  plugins: ["@typescript-eslint", "simple-import-sort", "import"],
  ignorePatterns: ["node_modules/", ".eslintrc.js"],
  rules: {
    "react/jsx-curly-brace-presence": "warn",
    "simple-import-sort/imports": "error", //importとexportのソート
    "simple-import-sort/exports": "error",
    "import/first": "error",
    "import/newline-after-import": "error",
    "import/no-duplicates": "error",
    "@typescript-eslint/consistent-type-definitions": ["warn", "type"], //型定義はtypeを使う
    "@typescript-eslint/no-explicit-any": "error", //any禁止
    "@typescript-eslint/no-unused-vars": "error", //未使用の変数禁止
    "react/self-closing-comp": ["error", { component: true, html: true }], //<Component />のように自己閉タグを使う
    "no-control-regex": "off", //正規表現中のASCII制御文字ok
    "react/jsx-boolean-value": "error", //attribute={true} → attribute
    "react/jsx-pascal-case": "error", //コンポーネント名はパスカルケース
    "object-shorthand": ["warn", "properties", { avoidQuotes: true }],
    "eslint-comments/require-description": "error", //eslint-disable-next-lineのコメントは必ず説明を書く。https://mysticatea.github.io/eslint-plugin-eslint-comments/rules/require-description.html
    "eslint-comments/disable-enable-pair": ["error", { allowWholeFile: true }], //https://mysticatea.github.io/eslint-plugin-eslint-comments/rules/disable-enable-pair.html
    "import/no-default-export": "error", //default export禁止
    "no-nested-ternary": "error", //三項演算子のネスト禁止
    "react/function-component-definition": [
      "error", //関数コンポーネントの定義はアロー関数を使う
      { namedComponents: "arrow-function" },
    ],
    "no-magic-numbers": [
      "error",
      {
        ignore: [-1, 0, 1], //配列検索でindexOf === -1などは許容する
        ignoreDefaultValues: true, //const { tax = 0.1 } = props
        ignoreArrayIndexes: true, //data[100] ok
        enforceConst: true, //マジックナンバーはconstで定義する
      },
    ],
    "@typescript-eslint/naming-convention": [
      "error",
      {
        selector: ["variable", "method", "accessor"], //基本的に全てcamelCase
        format: ["camelCase", "snake_case"],
      },
      {
        selector: ["property"], //APIリクエスト時にPascalCaseとなっている箇所がある
        format: ["camelCase", "snake_case", "PascalCase"],
      },
      {
        selector: "variable", //exportされている定数やコンポーネント
        modifiers: ["exported", "const"],
        format: ["PascalCase", "strictCamelCase"],
      },
      {
        selector: "interface", //interfaceはIをつけない
        format: ["PascalCase"],
        custom: { regex: "^I[A-Z]", match: false },
      },
      { selector: ["class", "typeAlias", "enum"], format: ["PascalCase"] },
      {
        selector: ["objectLiteralProperty"], //api requestのheadersの'Content-Type'などが対応するためnullで許容する
        format: null,
        modifiers: ["requiresQuotes"],
      },
    ],
  },
  overrides: [
    // Next.jsのファイルルーティングはexport defaultが必要
    {
      files: ["*/pages/**/**.tsx"],
      rules: {
        "import/no-default-export": "off",
        "import/prefer-default-export": "error",
        "@typescript-eslint/naming-convention": "off",
      },
    },
    {
      files: ["*/**/types/**/schema.ts"],
      rules: { "no-magic-numbers": "off" }, // schemaファイルでは許容する
    },
    // Storybookのファイルはdefault exportを許可
    {
      files: ["*/**/**.stories.tsx"],
      rules: { "import/no-default-export": "off" },
    },
  ],
};
.husky/pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
.lintstagedrc.js
const path = require("path");

const buildEslintCommand = (filenames) =>
  `next lint --file ${filenames.map((f) => path.relative(process.cwd(), f)).join(" --file ")}`;

module.exports = {
  "*.@{js,jsx,ts,tsx}": [buildEslintCommand, "prettier --write"],
};
package.json
package.json
{
  "scripts": {
    "lint": "next lint",
  }
}

以下の方法は私の環境では動きませんでした

{
  "scripts": {
    "lint-staged": "lint-staged"
  }
}

やったこと

  • 独自定義したルール一覧
    • eslint-disable を使う場合はコメント必須!
    • マジックナンバー禁止
    • import/export の sort
    • 型定義は typ を使う
    • any 禁止
    • 未使用の変数を残さない
    • 自己閉じタグを使う
      • <Component />
    • boolean の属性表記は省略形
    • 省略可能な object key は省略する
      • {data: data} → {data}
    • 原則 default export 禁止
    • アロー関数を使う
    • 命名規則の設定
  • lint 対象をsrc/とする
  • ステージングされた git ファイルに対してのみ lint を実行する(.lintstagedrc.js)

参考

こちらも!👩‍🍳

https://zenn.dev/tara_is_ok/articles/75f17beaef6ee8

Discussion