TypeScript プロジェクトに ESLint・Prettier・cspell を 1 つずつ入れて挙動を確かめてみた
製造ビジネステクノロジー部の小林です。
普段何気なく Linter を使っていますが、「結局それぞれ何をしているのか」が曖昧なままだったので、ゼロから TypeScript プロジェクトを立ち上げ、ESLint・Prettier・cspell を 1 つずつ入れて挙動を確かめてみました。
Linter とは
Linter は、ソースを実行せずに読むだけで問題を見つけてくれるツールです。これを静的解析と呼びます。
レビューで人間が気づくような「ここ any 使ってない?」「インデント揃ってないよ」「タイポしてるよ」という指摘を、機械が全ファイルチェックしてくれるイメージです。
Linter 系ツールの棲み分け
「Linter」とまとめて呼ばれがちですが、厳密にはそれぞれ役割が違います。
| ツール種別 | 何をする? | 例 |
|---|---|---|
| Linter | コードの「品質」「規約違反」を検出 | ESLint |
| Formatter | コードの「見た目」を自動整形 | Prettier |
| Spell Checker | スペルミスを検出 | cspell |
| Type Checker | 型の整合性を検証 | TypeScript Compiler |
| Test Runner | コードを実行して挙動を検証 | Jest, Vitest |
守備範囲を図にするとこんな感じです。
今回は、ESLint、Prettier、cspell の 3 つを順に入れていきます。
環境準備
前提
- Node.js v22
- パッケージマネージャは pnpm を使用(npm/yarn でも同様)
- macOS で動作確認
プロジェクトを作る
まずは作業ディレクトリを用意します。
mkdir linter && cd linter
pnpm init

TypeScript を入れる
題材は TypeScript なので、tsc を入れて tsconfig.json を初期化します。
pnpm add -D typescript @types/node
pnpm exec tsc --init

package.json の scripts に型チェック用のコマンドを足しておきます。
{
"scripts": {
"check:type": "tsc --noEmit"
}
}
わざとダメなコードを置く
linter の挙動を確かめるための題材コードを src/sample.ts として用意します。
var counter = 0;
const recieveMsg = function (msg: any) {
console.log(msg);
return msg;
};
短いコードですが、ルール違反が仕込んであります。
ESLint を入れてみる
インストール
ESLint と TypeScript 用プラグイン、それと Flat Config を扱いやすくする @eslint/js を入れます。
pnpm add -D eslint @eslint/js typescript-eslint

最小の eslint.config.js
ESLint v9 以降は Flat Config が標準です。配列の各要素が「ルールセット」で、files で対象を絞っていく仕組みです。
まずは最小限から始めます。
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.ts"],
rules: {
"no-console": "error",
},
},
];
ここで rules に書いているのは no-console だけ です。これは意図的なもので、js.configs.recommended と tseslint.configs.recommended を読み込んだ時点で、以下のルールはすでに有効になっているからです。
recommended は、それぞれが推奨ルールの詰め合わせです。
js.configs.recommended … @eslint/js が提供する、ESLint 公式の推奨ルールセット。no-var など JavaScript 全般のルールが入っています。
https://eslint.org/docs/latest/rules/
tseslint.configs.recommended … typescript-eslint が提供する、TypeScript 向けの推奨ルールセット。@typescript-eslint/no-explicit-any など型を絡めたルールが入っています
https://typescript-eslint.io/users/configs/#recommended
| ルール | 含まれている recommended |
|---|---|
no-var |
eslint:recommended |
@typescript-eslint/no-unused-vars |
typescript-eslint:recommended |
@typescript-eslint/no-explicit-any |
typescript-eslint:recommended |
no-console だけはどちらの recommended にも入っていないので明示的に有効化しています。
files の効きどころ
Flat Config の files は、それが書かれているオブジェクトの中だけ で効きます。配列全体に掛かるわけではありません。
今回の設定をもう一度見てみます。
export default [
{ ignores: ["dist/**", "coverage/**"] },
js.configs.recommended, // files なし → 全ファイルに適用
...tseslint.configs.recommended, // files なし → 全ファイルに適用
{
files: ["**/*.ts"], // このブロック内だけ .ts に限定
rules: {
"no-console": "warn",
},
},
];
files が書かれているのは最後のブロックだけなので、適用範囲は次のようになります。
| 設定 | 適用範囲 |
|---|---|
| js.configs.recommended | 全ファイル |
| tseslint.configs.recommended | 全ファイル |
| カスタム rules(no-console) | .ts のみ |
除外設定(ignores)
設定の先頭にある { ignores: [...] } が除外指定です。Flat Config では、ignores だけを持つオブジェクトを置くと、それが全体に効く除外設定になります。
node_modules はデフォルトで除外されるので、dist やカバレッジ出力など、自分のプロジェクト固有のものだけ書けば十分です。
scripts への登録
package.json の scripts に登録します。
{
"scripts": {
"check:lint": "eslint . --cache",
"fix:lint": "pnpm check:lint --fix"
}
}
--cache を付けると 2 回目以降の実行が速くなりますが、.eslintcache というファイルが生成されます。コミットする必要はないので .gitignore に追加しておきましょう。
動かしてみる
pnpm check:lint
実行すると、わざとダメに書いた sample.ts に対して以下のようなエラーが出ました。

src/sample.ts
error Unexpected var, use let or const instead no-var
error 'counter' is assigned a value but never used @typescript-eslint/no-unused-vars
error 'recieveMsg' is assigned a value but never used @typescript-eslint/no-unused-vars
error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any
warning Unexpected console statement no-console
recommended を読み込むだけでこれだけのチェックが効いています。また、自分で追加した 1 行(no-console)もきちんと機能していることが確認できます。
内部で何をしているのか
最初は「コードを文字列として、正規表現で見ているのかな?」と思っていました。var という文字を探しているだけ、のようなイメージです。でも実際は違いました。
ESLint は、ソースコードを一旦 AST(抽象構文木) という構造化されたデータに変換します。AST は、コードを「変数宣言」「関数」「呼び出し」といった意味のかたまりとして木構造で表現したものです。
ESLint はこの木を上からたどり、各ルールが「自分の探しているパターンに一致するノード」を見つけたら違反として報告します。
ざっくり流れにするとこうです。
例えば no-var ルールは、AST の中から VariableDeclaration ノードで kind === "var" のものを探す、という仕組みです。「構文として理解した上で違反箇所を検出している」というのがポイントです。
自動修正してみる
--fix で直せるのは fixer が定義されているルールだけ です。実際に試してみます。
pnpm fix:lint


実行前に出ていた no-var のエラーが消えました。var → let への書き換えは機械的に決まるため、自動修正されました。一方で残った 3 種は、いずれも手で直す必要があります。
| ルール | 自動修正 | 理由 |
|---|---|---|
| no-var | 直った | var → let / const への置換は機械的に決まる |
| no-unused-vars | 残る | 変数を消すか使うかは、書き手の意図次第 |
| no-explicit-any | 残る | 何の型にすべきかは文脈次第で、機械的に決められない |
| no-console | 残る | 消す・残す・別のログ手段にするなど、判断が必要 |
Prettier を入れてみる
ESLint が品質を見るツールなら、Prettier は見た目を整えるツールです。
ESLint と何が違うのか
| ESLint | Prettier | |
|---|---|---|
| 目的 | コード品質 | コード整形 |
| 検出例 | anyの使用、未使用変数 |
インデント、改行、クォート |
| 思想 | 設定可能 | 基本ノンカスタマイズ |
インストール
pnpm add --save-dev --save-exact prettier

.prettierrc.json を書く
ルートに .prettierrc.json を置きます。設定項目は少なめで OK です。
{
"trailingComma": "all",
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"printWidth": 80
}
| 設定 | 効果 |
|---|---|
trailingComma: "all" |
末尾カンマを必ずつける。diff が綺麗になる |
tabWidth: 2 |
インデント 2 スペース |
semi: true |
文末セミコロン必須 |
singleQuote: false |
ダブルクォート "..." 統一 |
printWidth: 80 |
80 文字超えたら改行 |
scripts に登録
{
"scripts": {
"check:format": "prettier --check --cache \"./**/*.{ts,tsx}\"",
"fix:format": "prettier --write --cache \"./**/*.{ts,tsx}\""
}
}
動かしてみる
pnpm check:format

整形前後の差分を見たいので、わざとフォーマットを崩したコードを用意してみます。
// 整形前
// これは80文字を超えるので縦に展開される
const user = { name: "taro", age: 30, hobbies: ["music", "sports", "reading", "cooking"],};
pnpm fix:format を流すと…

// Prettier整形後
const user = {
name: "taro",
age: 30,
hobbies: ["music", "sports", "reading", "cooking"],
};
「printWidth: 80 を超えるオブジェクトリテラルは縦に展開される」というルールで、毎回統一的に整形してくれます。
ESLint との競合をどう解決するか
ESLint にもフォーマット系のルール(インデントとか改行とか)があるので、何もしないと Prettier と ESLint が同じ箇所で違うことを言い出します。
これを解決するために eslint-config-prettier を入れます。
pnpm add -D eslint-config-prettier

eslint.config.js に追記します。
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintConfigPrettier from "eslint-config-prettier";
export default [
{ ignores: ["dist/**", "coverage/**"] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.ts"],
rules: {
"no-console": "warn",
"prefer-arrow-callback": "error",
"func-style": ["error", "expression", { allowArrowFunctions: true }],
},
eslintConfigPrettier, // ← 必ず後ろに置く
},
];
これは 「ESLint のフォーマット系ルールを全部 OFF にする」設定 です。
役割分担を図にするとこうなります。
フォーマットは Prettier に完全委譲、ESLint は品質に専念という棲み分けです。
cspell を入れてみる
最後はスペルチェッカーです。
なぜスペルチェックが必要か
// よくあるタイポ
const recieveMessage = () => {}; // 正: receive
const lenght = arr.length; // 正: length
こういうコードを未然に防いでくれます。コードだけではなくてマークダウンファイルのタイポも防いでくれます。
インストール
pnpm add -D cspell
cspell.json を書く
ルートに cspell.json を置きます。最初は空っぽで OK です。
{
"words": [],
"ignorePaths": []
}
scripts に登録します。
{
"scripts": {
"check:spell": "cspell \"**/*\" --cache --gitignore"
}
}
動かしてみる
題材コードには、わざとスペルミスを仕込んだ recieveMsg(正しくは receiveMsg)があります。この状態で CSpell を実行します。
pnpm check:spell


recieveMsg がスペルミスとして検出されます。
src/sample.ts:2:7 - Unknown word (recieve) fix: (receive)
ただ、このとき pnpm-lock.yaml まで検査対象に含まれてしまい、大量の指摘が出ることがあります。
lock ファイルにはパッケージ名やハッシュ値のような文字列が大量に並んでいるため、それらが軒並み「知らない単語」として検出されてしまうのです。lock ファイルは自動生成されるもので人間が綴りを気にする対象ではないので、次に説明する ignorePaths で検査対象から外します。
プロジェクト固有の単語を登録する
ライブラリ名や独自ドメイン用語は辞書にありません。cspell.json の words に追加して、辞書を育てていきます。あわせて、先ほどの pnpm-lock.yaml のように 検査しても意味がないファイルは ignorePaths で除外 します。
{
"words": [""],
"ignorePaths": ["pnpm-lock.yaml", "node_modules/**", "dist/**"]
}
この状態で動かしてみます。

pnpm-lock.yaml のエラーが消えました。続いて recieveMsg のスペルミスを直して実行します。

エラーが全て消えました!
モノレポでは辞書を分割できる
モノレポ構成では、ルートだけでなく各パッケージにも cspell.json を置けます。そして CSpell は、チェック対象ファイルに近い場所の設定から順に適用してくれます。
./cspell.json ← プロジェクト全体
./packages/backend/cspell.json ← backend独自
./packages/infrastructure/.../cspell.json ← 構成要素独自
これの何が嬉しいかというと、「そのパッケージでしか使わない用語」を、そのパッケージの中だけで許容できる点です。
ルートに「共通の用語」、各パッケージに「そこ固有の用語」と置き場所を分けることで、ルートの cspell.json の辞書の肥大化を防げます。
3 つを統合する
ここまでで ESLint・Prettier・cspell を入れたので、最終的な package.json の scripts は次のようになります。
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"check:type": "tsc --noEmit",
"check:lint": "eslint . --cache",
"fix:lint": "pnpm check:lint --fix",
"check:format": "prettier --check --cache \"./**/*.{ts,tsx}\"",
"fix:format": "prettier --write --cache \"./**/*.{ts,tsx}\"",
"check:spell": "cspell \"**/*\" --cache --gitignore"
}
}
命名ルール
スクリプト名に命名規則を設けておくと運用がスムーズです。
check:*→ チェックのみ行い、ファイルは変更しないfix:*→ チェックに加えて自動修正まで実行する
「壊さない(チェックだけの)コマンド」と「直してくれる(自動修正する)コマンド」を明確に分けるのがポイントです。
CI/CD への組み込み
プルリクエスト作成時に CI で以下を自動実行するようにすると、レビュー前の機械チェックが一通り走ります。
pnpm check:format
pnpm check:lint
pnpm check:type
pnpm check:spell
まとめ
今回は、ESLint・Prettier・cspell を実際にゼロから入れて触ってみました。最初に厳しいルールを入れるのではなく、開発を進めながら育てていく感じがよいのかなと思いました。








