🐳

[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