@typescript-eslint ことはじめ

追記: 2019/05/01

最近の@typescript-eslint/eslint-plugin, Prettierのアップデートによって、正式にTypeScript 3.4対応されました。一通り普通に動いてる感じ。

これまで自分で使っていて問題になったのがパフォーマンス。ESLint単体やTSLintに比べて、プロジェクトによるだろうけど2倍以上遅い。体感で結構辛い。

これはTypeScriptのパーサーを通す部分の構造上の問題が原因なので、簡単には直せなそうな気配。 とりあえず、個人的には型を必要とするルールにそこまでの価値を感じていないので、該当ルールを無効にしてparserOptions > projectを削除することにしました。

追記終わり


JavaScript用のlinterはESLintが絶対的な地位を確立している一方で、TypeScript用はTSLintがメジャーではあるものの、ドキュメントがひどい、recommendedがopinionatedすぎる、OSSとしてのガバナンスがアレなど微妙な状況だった(ESLintチームのレベルが高すぎるので比べるのは酷だけど)。

そんな中、先月ESLintチームおよびMS TypeScriptチームが ESLintのTypeScript対応本気出します宣言 したので、時代は今後ESLintに傾いていくのが確実視されている状況。ということで現時点ではどんなもんか試してみたら普通に思ったより使えたという話を書きます。

先に出来上がった自分用設定はこちら。shareable configで他の設定と組み合わせて使う前提なので、参考になるかは不明です。

github.com

コンセプト

TypeScript固有のルール以外は、JavaScript開発でESLintするときと設定を共通にしておきたい。JavaScriptの基本設定にとどまらず、ESLintエコシステムにはReact, JSDoc, security, a11yなど幅広いプラグインが揃っている。また定番ゆえにエディタやIDEの支援も充実している。TypeScript開発でもこれらを活用してDXを高めたい。

それから、現代においてはstylistic issuesはPrettierに任せ、linterはpossible errorsとbest practicesの検出・修正に徹するのが定石。Prettierなら必ずautofixできるし、bikeshedな議論 をする必要もなくなるので、思考停止して受け入れさえすればハッピーになれる。

結論として以下の3レイヤーになる。

  1. ESLintと各種ESLintプラグインによる共通ルール
  2. @typescript-eslintによるTypeScript固有のルール
  3. Prettierによるフォーマット

ただ個人的にはTypeScript固有のlintルールはほとんど必要ないと思っていて、ESLintのルールが動く(+Prettierをフックする)ことが主目的。

基本設定

インストール。TypeScript用に増えたのは @typescript-eslint/eslint-plugin だけ。

$ npm i -D eslint @typescript-eslint/eslint-plugin prettier eslint-config-prettier eslint-plugin-prettier

.eslintrc.jsの設定例

module.exports = {
  "extends": [
    "eslint:recommended", // お好きなESLint設定をここに
    "plugin:prettier/recommended"
  ],
  "plugins": [
    "@typescript-eslint"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    "sourceType": "module",
    "project": "./tsconfig.json"
  },
  "rules": {
    // お好みのルール設定を
    "@typescript-eslint/adjacent-overload-signatures": "error"
    // ...
  }
}

eslint:recommendedのところはベースにするお好みのESLint用のconfigを指定する。airbnbでもオレオレconfigでも良い。

plugin:prettier/recommendedは、Prettierと競合するルールを自動で無効にしつつ、ESLintの中でPrettierを動くようにしてくれる便利設定。ここまではTypeScript関係ない。

plugins, parserがTypeScript用の設定。parserOptionsは、TSではimportを使うのが普通なのでsourceType: moduleを指定。ecmaVersionは不要。projectは後述。Reactやる人はjsx:trueを追加。詳細はparserのREADMEを参照。

あとはrulesを定義していけばOK。

plugin:@typescript-eslint/recommendedを使えば多少行数減るのだけど、前述したように余計なTypeScript用のルールを追加したくないので個人的にはやらない。

Tipsいろいろ

雑に書いていきます。

ルールを調べるときは ROADMAP.md を見るべし

TSLintのあのルールはESLintでは何?実装状況は?どのルール使えば良い?というとき、@typescript-eslint/eslint-pluginREADMEを見てもルールがアルファベット順に並んでるだけで微妙。

実は、TSLintや各種プラグインも含めた網羅性のあるルール対照表がこっそりとROADMAP.mdというファイルに記載されている。現時点ではこれが最重要ドキュメント。

TypeScript固有のルールについては、これを見ながらTypeScript-specificというところから好きなルールを選んでいく感じになる。

ただし、前述したように個人的にはここでそんなに縛る必要はないと思っていて、面倒な人は何も指定しなくても良いと思う。不要なtype assertionを修正するno-unnecessary-type-assertionぐらいか?

上書きされるESLintルールを無効に

ESLint本家のルールは思いのほかTypeScriptでも順調に動くものの、いくつかはTypeScriptでは期待通りに動かないルールがある。例えばno-unused-varsは型やinterfaceをうまく扱えずfalse positiveな誤検知をしてしまうケースがある。

そこで@typescript-eslint/eslint-pluginではそのような本家ルールをTypeScriptでもうまく動くように修正版ルールを定義している。

  • @typescript-eslint/camelcase
  • @typescript-eslint/indent
  • @typescript-eslint/no-array-constructor
  • @typescript-eslint/no-unused-vars
  • @typescript-eslint/no-use-before-define
  • @typescript-eslint/no-useless-constructor

注意点として、これらのESLint本家版ルールを有効にしている場合(例えばeslint:recommendedを使っている場合はno-unused-varsが有効)は、以下のように一度無効にしてからTypeScript版を有効にする必要がある。修正版は本家とは別のルールとして定義されているので、本家を無効にしないとエラーが重複して報告されてしまうため。オプションを指定してる場合はその再指定も忘れずに。

{
  "no-unused-vars": "off",
  "@typescript-eslint/no-unused-vars": "error"
}

plugin:@typescript-eslint/recommendedの中でも同様の設定がされている。

この上書きルールリストは提供されてない(上のは自分でスクリプトを書いて生成した)し、手動も辛いので自動でいい感じになってほしい。このあたりをwatchしとく。

eslint-plugin-node.ts有効化

定番プラグインeslint-plugin-nodeの便利ルール、node/no-missing-import等はデフォルトでは拡張子.tsのファイルを見てくれないのでエラーが誤検知されてしまう。そんなこともあろうかとtryExtensionsオプションが用意されているので追加してあげればOK。

shared settingに書くと同種のルールに一括適用できるので便利。Reactやる人は.tsxも追加しておく。

"settings": {
  "node": {
    "tryExtensions": [".ts", ".js", ".json", ".node"]
  }
},
"rules": {
  "node/no-missing-import": "error"
}

no-dupe-class-membersを無効に

TypeScriptでメソッドオーバーロードすると誤検知されてしまうため。

これもいい感じになんとかしてほしい気持ち。issue登録しといた。

parserOptions > project

追記(2019/05/01): 冒頭に追記した通り、現状ではprojectを有効にすると極端にパフォーマンスが落ちるので注意。個人的にはそこまでの価値を見出せてないので利用をやめました。追記終わり。


型情報を必要とするルールを使う場合に要求されるtsconfig.jsonを指定するオプション (see README)。相対パスの起点はtsconfigRootDirオプションであり、そのデフォルトはcwdになっている模様。なので、shareable configでconfig: "./tsconfig.json"と書いても、tsconfig.jsonがあるプロジェクトルートでeslintを実行すればうまく動く。

逆にtsconfig.jsonの位置がおかしいmonorepo構成やVS CodeなどIDE経由の実行だと相対パスではうまくいかないケースがあるらしく、その場合は絶対パスを指定することになりそう。

自動でいい感じにしてほしい気持ちなのでこの辺をwatch

Prettierとのルール衝突の防止

eslint-config-prettierに衝突するルールを無効にする専用ファイルが提供さてれている

これはeslint-plugin-prettierのrecommendedには入ってないので、使う場合は別途指定する。 ただし、Prettierを使う前提ならstyle系ルールは@typescript-eslintで有効にしないだろうし、shareable configを作らないとextendsで上書きできないので、これを使う場面は限られるかも。

感想

思っていたよりも普通にESLintルールがTypeScriptで動いて驚いた。細かいバグはパラパラあるけど対応しやすいし動きは活発なので常用していく気持ちになった。

いくつか書いたように多少面倒な手動設定を要求されるところが残っていたり、ドキュメント整備もこれからやっていくみたいなので、注目度高いしESLintチームは対応丁寧だし良いOSS貢献チャンスっぽい気がしますがみなさんどうですか?


追記

もうTSLintはダメなの?ESLintに乗り換える必要ある?フロントは流れが速い、などの声を察したので一応書いておくと、linterの設定なんてプロジェクトの初期にやればほぼ終わりなので、既存のプロジェクトを急いで書き換える必要は全くないです。普通の人はこんな本質的ではないyak shavingを頑張る必要ないので、あと半年も経って諸々こなれてきた頃、新規プロジェクトを作る機会があればググった設定をコピペしたら良いのではと思います。