TypeScript + Node.jsプロジェクトにESLint + Prettierを導入する手順2020

# TL;DR

https://github.com/notakaos/typescript-node-base-with-eslint-prettier

完成形のソースコードはこちら↑

# この記事の趣旨

[TypeScript + Node.js プロジェクトのはじめかた2020](https://qiita.com/notakaos/items/3bbd2293e2ff286d9f49) で作成したTypeScript + Node.jsのプロジェクトに `ESLint` / `Pretiter` / `husky & lint-staged` を導入する手順を紹介します。

今回導入するツールとバージョンは以下になります。

項目         | バージョン
------------|-----------
ESLint      | 7.6.0
Prettier    | 2.0.5
husky       | 4.2.5
lint-staged | 10.2.11

# 動作環境

node と npm はインストール済みとします。

```sh
$ node -v
v12.18.3
$ npm -v
6.14.6
```

また、今回の記事はmacOSにて検証しております。

```sh
$ uname -v
Darwin Kernel Version 19.6.0: Sun Jul  5 00:43:10 PDT 2020; root:xnu-6153.141.1~9/RELEASE_X86_64

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G73
```

Linux環境だと手順はほぼ同じだと思いますが、Windows環境の場合は読み替えが必要になるかもしれません。

# ESLint

https://eslint.org/

## ESLintについて

<ruby>ESLint<rt>イーエスリント</rt></ruby>はJavaScriptコードのエラーチェックを行うLinterと呼ばれるツールの一つです。

JavaScriptは動的な言語なので、実行するまでエラーがあるのかどうかわからない部分があります。
ESLintを使うことで、JavaScriptのコードを実行することなくコード上の問題を発見するのに役立ちます。

また、ESLintは本来JavaScript用ですが、TypeScript用のプラグインを追加することにより、TypeScriptでもESLintが使えるようになります。

以下ではTypeScript (+ Node.js)なプロジェクトへのESLintの導入手順を紹介します。

## ESLint (+ TypeScriptプラグイン)の導入

https://github.com/notakaos/typescript-node-base にESLintを導入します。

```sh
# typescript-node-base リポジトリをローカルで作成済みとします 
cd typescript-node-base
```

Node.jsプロジェクトにESLintとESLintのTypeScript用プラグインを追加します。

```sh:typescript-node-base
npm install -D eslint typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin

# ESLintのバージョン確認
./node_modules/.bin/eslint --version
#=> v7.6.0
```

次に、ESLint用のTypeScript設定ファイル `tsconfig.eslint.json` を作成します。

```sh:typescript-node-base
# tsconfig.eslint.json ファイルの作成
touch tsconfig.eslint.json

# エディターで編集(お好みのエディターをお使いください)
vim tsconfig.eslint.json
```

```tsconfig.eslint.json
{
  "extends": "./tsconfig.json",
  "include": [
    "src/**/*.ts",
    ".eslintrc.js"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}
```


そして、ESLint用の設定ファイル `.eslintrc.js` をプロジェクトディレクトリ直下に作成します。

```sh:typescript-node-base
# .eslintrc.js ファイルの作成
touch .eslintrc.js

# エディターで編集(お好みのエディターをお使いください)
vim .eslintrc.js
```

```js:~/typescript-node-base/.eslintrc.js
module.exports = {
  root: true,
  env: {
    es6: true,
    node: true,
  },
  parser: '@typescript-eslint/parser',
  parserOptions: {
    sourceType: 'module',
    ecmaVersion: 2019, // Node.js 12の場合は2019、他のバージョンのNode.jsを利用している場合は場合は適宜変更する
    tsconfigRootDir: __dirname,
    project: ['./tsconfig.eslint.json']
  },
  plugins: [
    '@typescript-eslint',
  ],
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
  ],
  rules: {
  },
};
```

なお、extendsでいくつかの項目がありますが、extendsはあらかじめ用意されたESLintルールのセットになります。これを入れることによって、以下のルールを適用しています。

* [eslint:recommended のルール一覧](https://eslint.org/docs/rules/)
* [plugin:@typescript-eslint/recommended のルール一覧](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/src/configs/recommended.ts)
* [plugin:@typescript-eslint/recommended-requiring-type-checking のルール一覧](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/src/configs/recommended-requiring-type-checking.ts)

これで、ESLint導入完了です。

## ESLintの実行

早速、ESLintを実行してみましょう。

```sh:typescript-node-base
npx eslint src/index.ts
```

```sh:実行結果
```

何も表示されなければ成功です！

設定やソースコードに問題がない場合、何も表示されないのが正常です。
もしここでエラーが表示される場合は記述ミスが考えられますので、各種設定ファイルやソースコードを見直してください。

次に、ESLintでエラーを検知できるか確認してみましょう。
`src/index.ts` を以下のように編集します。

```diff:src/index.ts
function hello(name: string): string {
  return `Hello, ${name}!`;
}

- console.log(hello('TypeScript'));
+ console.log(hello('TypeScript')
```

最後の行の閉じカッコを一つ削除して、文法エラーを意図的に発生させます。
ソースコードを書き換えたら保存し、ESLintを実行します。

```sh:typescript-node-base
npx eslint src/index.ts
```

すると以下のようなエラーが発生します。

```sh:実行結果
/Users/notakaos/typescript-node-base/src/index.ts
  6:0  error  Parsing error: ')' expected

✖ 1 problem (1 error, 0 warnings)
```

「パースエラー: `)` が期待されている(が存在しない)」というエラーがでましたね。
ESLintで文法チェックが正しく動いていることがわかります。

ちなみに、文法のチェックに関してはtscでも同様のチェックができます。
(--noEmitオプションをつけてjsファイルを生成しないようにします)

```sh:typescript-node-base
npx tsc --noEmit
```

```sh:実行結果
src/index.ts:6:1 - error TS1005: ')' expected.

6 
  


Found 1 error.
```

そのため、文法チェックだけだとtscのみでよいのでは？　と思われるかもしれませんが、ESLintにはtscに含まれていないエラーチェック機能が入っているため、tscとESLintを併用することによりエラーの検知率をあげることができます。

ここで、tscではエラーにならず、ESLintでエラーになる例を見てみましょう。
`src/index.ts` に以下の記述を追加します。

```diff:src/index.ts
function hello(name: string): string {
  return `Hello, ${name}!`;
}

- console.log(hello("TypeScript")
+ console.log(hello("TypeScript")); // 元に戻す


+ function example() {} // どこからも呼ばれない中身が空の関数を用意
```

まずはtscを実行してみます。

```sh
npx tsc --noEmit
```

```sh:実行結果
```

特にエラーもなく正常にコマンドが終了しました。
次にESLintを実行します。

```sh
npx eslint src/index.ts
```

```sh:eslint実行結果
/Users/notakaos/typescript-node-base/src/index.ts
  7:10  warning  'example' is defined but never used  @typescript-eslint/no-unused-vars
  7:20  error    Unexpected empty function 'example'  @typescript-eslint/no-empty-function

✖ 2 problems (1 error, 1 warning)
```

ESLintでは1つの警告と1つのエラーが発生しましたね。

警告・エラーの内容について、

- 「警告: example関数は定義されているものの使われていない」
- 「エラー: 空の関数は期待されていない」

といった内容になっています。

このように、tscでは通るもののESLintではエラーになるようなものもあり、
両者を適切に設定することでよりバグが少ないコードを書くことができるようになります。

最後に毎回 `eslint` / `tsc` のコマンドを手打ちすると面倒なので、簡単にエラーチェックができるようにpackage.jsonのscriptsに設定を追加します。

```diff:package.json
{
  ...
  "scripts": {
    "dev": "ts-node src/index.ts",
    "dev:watch": "ts-node-dev --respawn src/index.ts",
    "clean": "rimraf dist/*",
    "tsc": "tsc",
    "build": "npm-run-all clean tsc",
-   "start": "node ."
+   "start": "node .",
+   "check-types": "tsc --noEmit",
+   "eslint": "eslint src/**/*.ts",
+   "eslint:fix": "eslint src/**/*.ts --fix",
+   "lint": "npm-run-all eslint check-types"
  },
  ...
}
```

エラーチェックを実行する時は以下のコマンドを実行します。

```sh:typescript-node-base
npm run lint
```

こうすることで、一度のコマンド実行で `eslint` と `tsc` の両方を実行することができます。

また、以下のように個別に実行することもできます。

```sh:typescript-node-base
# tsc --noEmit だけ実行
npm run check-types 

# eslint だけ実行
npm run eslint

# eslint のエラーを(できるものだけ)自動修復
npm run eslint:fix
```

普段は `npm run lint` を使い、必要に応じて個別のコマンドを使い分けることになります。

# Prettier

https://prettier.io/

## Prettierについて

<ruby>Prettier<rt>プリティア</rt></ruby>はソースコードの整形ツール(コードフォーマッター)の一つです。
JavaScript/TypeScript/JSON等の形式に対応しています。

Prettierを使うことで、スペースやインデント、文字列のクオートの統一、1行が長くなりすぎた場合の改行位置調整などを自動で行ってくれます。

Prettier等のコードフォーマッターを使わない場合、実装者によってコードの書き方がばらばらになり、コードレビュー時にインデント位置やスペース等の指摘が入ってしまうことがあります。それだと本質的なディスカッションができません。そのため、フォーマッターを設定しておくことで、コードの書き方が統一され、ロジックのレビューに集中できるようになる効果があります。

ちなみに、ESLintにもコードフォーマット機能があるため、ESLintと併せてPrettierを利用する場合はESLint側のフォーマットルールをOFFにする必要があります。幸い、PrettierとESLintのフォーマットルールがぶつからないようにするためのルールセット(config)が用意されていますので、そちらも併せて導入します。

## Prettier (+ eslint-config-prettier) の導入

PrettierとESLintのPrettier configをインストールします。

```sh:typescript-node-base
# --save-exact でバージョンを固定してdevインストール
npm install -D --save-exact prettier eslint-config-prettier

# Prettierのバージョン確認
./node_modules/.bin/prettier --version
#=> 2.0.5
```

次に、Prettier用の設定ファイルをプロジェクトディレクトリ直下に作成します。

```sh:typescript-node-base
echo "{}"> .prettierrc.json
```

```sh:.prettierrc.json
{}
```
※ `.prettierrc.json` は必要に応じて書き換えてください

また、prettierの対象外となるファイルを指定する `.prettierignore` をプロジェクトディレクトリ直下に作成します。

```sh:typescript-node-base
touch .prettierignore

# エディターで編集
vim .prettierignore
```

```txt:.prettierignore
# Ignore artifacts:
/dist
node_modules
package.json
package-lock.json
tsconfig.json
tsconfig.eslint.json
```

このままだと、ESLintのコードフォーマット機能とPrettierのコードフォーマット機能がコンフリクトしてしまうので、`.eslintrc.js`のextendsに `prettier` と `prettier/@typescript-eslint` の記述を追加します(追加する位置はextendsの最後の方になります)。

```diff:.eslintrc.js
module.exports = {
  // ...
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:@typescript-eslint/recommended-requiring-type-checking',
+   'prettier',
+   'prettier/@typescript-eslint',
  ],
  // ...
}
```

これでPrettierの設定は完了です。

### コラム: eslint-plugin-prettier について

インターネット上でESlintとPrettierの導入方法を調べると、 `eslint-plugin-prettier` も一緒に入れる方法が紹介されていることがあります。

いつからかはわかりませんが、 [公式ドキュメント](https://prettier.io/docs/en/integrating-with-linters.html)によると、eslint-plugin-prettier が **一般的には推奨されなくなっている** ようです。

なぜ非推奨なのかというと、以下のような理由が挙げられています。

- エディターに赤い波線がたくさん表示され、煩わしくなる
- Prettierを直接実行するよりも遅くなる
- 物事が壊れる可能性がある間接層の1つになる

そのため、この記事では eslint-plugin-prettier を導入せず、eslintとは別にpretiterを実行する形を採用しております。

## Prettierの実行

では、Prettierを実行してみましょう。

まず `src/index.ts` を以下のようにインデントなどがバラバラな状態に書き換えます。

```typescript:src/index.ts
function
hello(name:string):string{
return `Hello, ${name}!`
}

console.log(hello("TypeScript"
))
```

そして、以下のコマンドでPrettierを実行します。

```sh:typescript-node-base
npx prettier --write src/index.ts
```

すると、以下のようにコードが修正されます。

```sh:src/index.ts
function hello(name: string): string {
  return `Hello, ${name}!`;
}

console.log(hello("TypeScript"));
```

きれいに整形されましたね。

## Prettierのnpm-scripts設定

あとは、prettierを簡単に実行できるようにpackage.jsonのscriptsに追記します。

Prettier単体で実行する `format` と、 `eslint` / `tsc` / `prettier` を1度に実行する `lint:fix` を追加しています。

```diff:package.json
{
  ...
  "scripts": {
    "dev": "ts-node src/index.ts",
    "dev:watch": "ts-node-dev --respawn src/index.ts",
    "clean": "rimraf dist/*",
    "tsc": "tsc",
    "build": "npm-run-all clean tsc",
    "start": "node .",
    "check-types": "tsc --noEmit",
    "eslint": "eslint src/**/*.ts",
    "eslint:fix": "eslint src/**/*.ts --fix",
+   "format": "prettier --write 'src/**/*.{js,ts,json}'",
-   "lint": "npm-run-all eslint check-types"
+   "lint": "npm-run-all eslint check-types",
+   "lint:fix": "npm-run-all eslint:fix check-types format"
  },
  ...
}
```

Prettierのみを実行する場合は以下のコマンドを実行します。

```sh:typescript-node-base
npm run format
```

`eslint`と`tsc`も併せて実行する場合は、以下のコマンドを実行します。

```sh:typescript-node-base
npm run lint:fix
```

これでPrettierの導入完了です。

# Git commit時に自動でlintを実行する(huskyとlint-staged)

これまでの手順でESLintとPrettierを導入しましたが、コードを修正するたびに `lint:fix` コマンドを手動で実行する必要がありました。
そのため、lintをかけていないコードを間違ってコミットしてしまうことがあります。

これを防ぐため、git commit時に `esLint`/ `tsc` / `prettier` を自動的に実行するように `husky` と `lint-staged` を導入します。

## huskyとは

https://github.com/typicode/husky

gitのcommit前やpush前などに特定のコマンドを実行するためのGit hooksを簡単に作成するためのツールです。後述のlint-stagedと組み合わせて使います。

## lint-stagedとは

https://github.com/okonet/lint-staged

`git add` でステージングに追加されたファイルに対して、指定したコマンドを実行します。

## huskyとlint-stagedの導入

huskyとlint-stagedは以下のコマンドを実行するとpackage.jsonに自動で設定が追加されます。

```sh:typescript-node-base
npx mrm lint-staged
```

```diff:package.json
{
  ...
  "devDependencies": {
    "@types/node": "^12.12.54",
    "@typescript-eslint/eslint-plugin": "^3.9.0",
    "@typescript-eslint/parser": "^3.9.0",
    "eslint": "^7.6.0",
    "eslint-config-prettier": "6.11.0",
+   "husky": "^4.2.5",
+   "lint-staged": "^10.2.11",
    "npm-run-all": "^4.1.5",
    "prettier": "2.0.5",
    "rimraf": "^3.0.2",
    "ts-node": "^8.10.2",
    "ts-node-dev": "^1.0.0-pre.56",
    "typescript": "^3.9.7"
- }
+ },
+ "husky": {
+   "hooks": {
+     "pre-commit": "lint-staged"
+   }
+ },
+ "lint-staged": {
+   "*.js": "eslint --cache --fix",
+   "*.{js,ts,json}": "prettier --write"
+ }
}
```

そのままではTypeScriptファイルにおいて、ESLintとtscが実行されないので、`lint-stage` の部分を少し修正します。

```diff:package.json
{
  ...
  "lint-staged": {
-   "*.js": "eslint --cache --fix",
+   "*.{js,ts}": "eslint --cache --fix",
+   "*.ts": "tsc --noEmit",
    "*.{js,ts,json}": "prettier --write"
  }
}
```

これで設定完了です。

### (2021/5/12 追記) lint-staged で tsconfig.json が効かない場合

[こちらのコメント](#comment-834e266881dd5b118244)で情報をいただいたのですが、lint-stagedで `tsc` コマンドを実行すると `tsconfig.json` が読み込まれず、 `tsc --noEmit` の結果で差異がでてしまうようです。

これは現状 `tsc` コマンドがファイルを指定した場合に tsconfig.json を見ないという仕様になっていることが起因しています。

* 参考: https://github.com/okonet/lint-staged/issues/825
* 参考: https://github.com/microsoft/TypeScript/issues/27379

MacやLinux環境でbashが使用できる場合は、tscをbash経由で実行することでtsconfig.jsonを見るようになるとのことです。

```diff_javascript:package.json
{
  ...
  "lint-staged": {
    "*.{js,ts}": "eslint --cache --fix",
-   "*.ts": "tsc --noEmit",
+   "*.ts": "bash -c tsc --noEmit",
    "*.{js,ts,json}": "prettier --write"
  }
}
```

もしくは、package.jsonにlint-stagedの設定を記述する代わりに `lint-staged.config.js` ファイルを作成し、関数を渡すことでこの問題を回避できるようです(こちらは未検証)。

```js:lint-staged.config.js
// lint-staged.config.js
module.exports = {
  "*.{js,ts}": [
    "eslint --cache --fix",
  ],
  "*.ts": [
    () => "tsc --noEmit", 
    "eslint --cache --fix",
  ],
  "*.{js,ts,json}": [
    "prettier --write"
  ],
}
```

* 参考: https://github.com/okonet/lint-staged/issues/825#issuecomment-620018284

## husky & lint-staged を試す

それではhuskyとlint-stagedを試してみましょう。

まず `src/index.ts` を以下のように書き換えます。

```ts:src/index.ts
function 
hello(name: string): string {
return `Hello, ${name}!`;
}

console.log(hello("TypeScript!!!!!")
);
```

そしてgit addを行い、その後git commitを実行します。

```sh:typescript-node-base
git add src/index.ts
git commit -m "Update src/index.ts"
```

```sh:実行結果
husky > pre-commit (node v12.18.3)
✔ Preparing...
✔ Running tasks...
✔ Applying modifications...
✔ Cleaning up... 
[feature/husky-lint-staged f69684a] Update src/index.ts
 1 file changed, 1 insertion(+), 1 deletion(-)
```

`eslint` / `tsc` / `prettier` の各コマンドが実行されたのがわかります。

そして `src/index.ts` を確認してみると、prettierによって自動整形されているのがわかります。

```ts:typescript-node-base
function hello(name: string): string {
  return `Hello, ${name}!`;
}

console.log(hello("TypeScript!!!!!"));
```

`git log -p` で差分を確認してみましょう。

```sh:typescript-node-base
git log -p
```

```diff:実行結果
...
--- a/src/index.ts
+++ b/src/index.ts
@@ -2,4 +2,4 @@ function hello(name: string): string {
   return `Hello, ${name}!`;
 }
 
-console.log(hello("TypeScript"));
+console.log(hello("TypeScript!!!!!"));
```

prettierでコードフォーマットされた後にcommitされていますね。

これでlintをかけずにcommitされることを防ぐことができます。

設定は以上です。

あとはお好みでカスタマイズしましょう！

# 成果物

以上の手順を実行した完成形のソースコードは以下のリポジトリをご参照ください。

https://github.com/notakaos/typescript-node-base-with-eslint-prettier

# 参考

- [typescript-eslint/README.md at master · typescript-eslint/typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/README.md)
- [typescript-eslint/TYPED_LINTING.md at master · typescript-eslint/typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/blob/master/docs/getting-started/linting/TYPED_LINTING.md#getting-started---linting-with-type-information)
- [TypeScript With Babel: A Beautiful Marriage](https://iamturns.com/typescript-babel/)
- [VSCode + TypeScript + ESLint + Prettier + Husky + lint-staged (+ JavaScript Standard Style) でコードを防衛@2019秋（2019/12/30 更新） - Qiita](https://qiita.com/ishiyama0530/items/c013475c563322965e2a)
- ["parserOptions.project" has been set for @typescript-eslint/parser - Stack Overflow](https://stackoverflow.com/questions/58510287/parseroptions-project-has-been-set-for-typescript-eslint-parser)
- [Install · Prettier](https://prettier.io/docs/en/install.html)
- [Integrating with Linters · Prettier](https://prettier.io/docs/en/integrating-with-linters.html)
- [Using ESLint and Prettier in a TypeScript Project - DEV](https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb)
- [VSCodeでESLint+typescript-eslint+Prettierを導入する v3.0.0 - Qiita](https://qiita.com/madono/items/a134e904e891c5cb1d20)

# 変更履歴

- 2020/8/12 記事公開