JavaScript開発者必見!ESLint「prefer-const」でコードを改善
ESLintの「prefer-const」ルールは、JavaScriptの変数宣言において、再代入されない変数をlet
ではなくconst
で宣言することを推奨するルールです。
JavaScriptでは、変数を宣言する方法として主にvar
、let
、const
の3つがあります。
const
: ES6で導入されたキーワード。ブロックスコープを持ち、宣言時に値を初期化する必要があり、一度値を代入すると再代入はできません。 (ただし、オブジェクトや配列の場合、その中身のプロパティや要素は変更可能です。const
が制限するのは変数自体への再代入です。)let
: ES6で導入されたキーワード。ブロックスコープを持ち、再代入は可能ですが、再宣言はできません。変数の値を後で変更する可能性がある場合に使用します。var
: 2015年のES6以前から存在したキーワード。関数スコープを持ち、巻き上げ(hoisting)の挙動が複雑で、再宣言や再代入が自由にできるため、意図しないバグの原因になることがありました。現代ではほとんど使われません。
なぜ「prefer-const」を使うのか?
「prefer-const」ルールが推奨される主な理由は以下の通りです。
- 意図の明確化
変数がconst
で宣言されている場合、その変数の値が後で変更されることはないという意図がコードを読む人に明確に伝わります。これにより、コードの理解が容易になります。 - バグの防止
変数に誤って再代入してしまうという種類のバグを防ぐことができます。例えば、ループ内でカウンター変数とは別に、意図せず再代入されてはいけない値が上書きされてしまうようなケースです。 - 可読性の向上
コードのどこで値が変更される可能性があるのか、どこは変更されないのかが視覚的に区別しやすくなります。 - 不変性の促進
関数型プログラミングのパラダイムでは不変性(immutability)が重視されます。「prefer-const」はその原則に沿ったコーディングを促します。
「prefer-const」の適用例
例えば、以下のようなコードがあるとします。
// prefer-const ルールが有効な場合、警告/エラーになる例
let name = "Alice"; // 'name' is never re-assigned. Use 'const' instead.
console.log(name);
let age = 30; // 'age' is never re-assigned. Use 'const' instead.
// ... age を変更する処理がない場合
console.log(age);
// prefer-const ルールが適用されない例
let counter = 0;
counter++; // counter が再代入されているので OK
console.log(counter);
const PI = 3.14; // 最初から const で宣言しているので OK
上記の例で、「name」と「age」はlet
で宣言されていますが、その後どこでも再代入されていません。この場合、「prefer-const」ルールが有効になっていると、ESLintは「let
ではなくconst
を使いましょう」という警告またはエラーを出力します。
設定方法
ESLintの設定ファイル(通常は.eslintrc.js
や.eslintrc.json
など)に、以下のようにルールを追加します。
{
"rules": {
"prefer-const": "error" // または "warn"
}
}
"warn"
: ルールに違反した場合、ESLintは警告として報告しますが、通常はビルドを停止しません。"error"
: ルールに違反した場合、ESLintはエラーとして報告し、多くのLinterはビルドを停止させます。
エラーメッセージ: 'variableName' is never reassigned. Use 'const' instead. prefer-const
説明
これは prefer-const
ルールが最も典型的に出すエラーメッセージです。let
で宣言された変数が、そのスコープ内で一度も再代入されていない場合に発生します。「この変数は変更されないのだから、const
を使うべきですよ」というESLintからの指摘です。
よくある原因
- グローバル変数や外部からの変更
宣言されたスコープ内では再代入されていないが、実はコールバック関数や外部のスコープから間接的に変更されているケース。しかし、prefer-const
は静的解析(コードを読み込むだけで判断する)であるため、このような動的な変更を検知できません。 - 単純な見落とし
変数をlet
で宣言したものの、後で値を変更する予定がなくなったり、そもそも変更する必要がない変数だったりする。
トラブルシューティング
-
意図的に let を使う場合(稀なケース)
- 本当に将来的に再代入の可能性がある場合
現時点では再代入されていなくても、将来的にコードが変更されて再代入される可能性がある場合(例えば、開発中の機能でまだ実装されていない部分など)は、let
のままで構いません。ESLintの警告を一時的に無効にするコメント(// eslint-disable-next-line prefer-const
)を追加することもできますが、乱用は避けるべきです。 - 複雑な初期化ロジック
変数の初期化が複数ステップに分かれていて、宣言と同時に初期化できない場合など。
このようなケースでは、ESLintのlet timer; function initialize() { if (condition) { clearInterval(timer); } } timer = setInterval(initialize, 100); // 宣言後に代入されているので let でOK
prefer-const
ルールにはignoreReadBeforeAssign
オプションがあります。これをtrue
に設定すると、宣言と最初の代入の間に変数が参照されている場合、その変数を無視します。{ "rules": { "prefer-const": ["error", { "ignoreReadBeforeAssign": true }] } }
- 本当に将来的に再代入の可能性がある場合
-
デストラクチャリングの場合
- 再代入される変数とされない変数を分ける:
// 修正例 let { a } = someObject; const { b } = someObject; // b は const に a = 10; console.log(b);
prefer-const
ルールのオプションdestructuring
を調整する。デフォルトは"any"
で、デストラクチャリングされた変数の一部でもconst
にできるものがあれば警告します。これを"all"
に設定すると、デストラクチャリングされた変数すべてがconst
にできる場合にのみ警告します。
ただし、これはすべてのケースで望ましいとは限らないため、注意が必要です。// .eslintrc.js または .eslintrc.json { "rules": { "prefer-const": ["error", { "destructuring": "all" }] } }
- 再代入される変数とされない変数を分ける:
-
const への変更
メッセージの通り、その変数が本当に再代入されないのであれば、let
をconst
に変更します。これが最も推奨される解決策です。// 修正前 let userName = "Taro"; console.log(userName); // 修正後 const userName = "Taro"; console.log(userName);
オブジェクトや配列のプロパティの変更に関する誤解
説明
const
で宣言された変数にオブジェクトや配列が代入されている場合、その変数自体に別のオブジェクトや配列を再代入することはできません。しかし、そのオブジェクトや配列の中身のプロパティや要素を変更することは可能です。
const myArray = [1, 2, 3];
myArray.push(4); // OK: 配列の中身を変更
console.log(myArray); // [1, 2, 3, 4]
// myArray = [5, 6]; // エラー: const 変数への再代入
この挙動を誤解していると、「const
を使ったのに中身が変わるじゃないか!」と混乱することがあります。prefer-const
ルールは、変数そのものへの再代入が行われていないかを見ているため、上記の myArray.push(4)
のような操作では警告を出しません。
トラブルシューティング
- 不変性を強制したい場合
もしオブジェクトや配列の中身も不変にしたいのであれば、Object.freeze()
を使用するなどの追加の対応が必要です。const myImmutableArray = Object.freeze([1, 2, 3]); // myImmutableArray.push(4); // エラー: Cannot add property 3, object is not extensible
- const の意味の再確認
const
は「値が不変(immutable)」であるというよりは、「変数のバインディング(参照先)が不変」であると理解することが重要です。参照先のオブジェクト自体が可変(mutable)であるかどうかは、const
とは別の概念です。
設定ファイル(.eslintrc.*)の問題
説明
ESLintが正しく設定されていない場合、prefer-const
ルールが期待通りに動作しないことがあります。
よくある原因
- ESLintのキャッシュ
ESLintはパフォーマンス向上のためにキャッシュを使用します。設定を変更しても、キャッシュが原因で古い情報が使われてしまうことがあります。 - パーサーの設定ミス
特にTypeScriptを使用している場合、適切なパーサー(@typescript-eslint/parser
など)が設定されていないと、ESLintがコードを正しく解析できず、ルールの適用に失敗することがあります。 - ルールが有効になっていない
設定ファイルにprefer-const
が追加されていない、または"off"
に設定されている。
トラブルシューティング
- キャッシュのクリア
ESLintのキャッシュをクリアするために、node_modules/.cache/eslint
ディレクトリを削除したり、eslint --fix --no-cache
のように--no-cache
オプションを付けて実行してみます。 - 設定の確認
.eslintrc.js
または.eslintrc.json
を開き、rules
セクションに"prefer-const": "error"
または"prefer-const": "warn"
が含まれていることを確認します。- TypeScriptを使っている場合は、
parser
に@typescript-eslint/parser
が設定されていることを確認します。{ "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", // または適切なESバージョン "sourceType": "module" }, "plugins": [ "@typescript-eslint" ], "rules": { "prefer-const": "error" // ... 他のルール } }
実行環境やエディタの統合の問題
説明
ESLintは正しく設定されているのに、エディタ(VS Codeなど)でリアルタイムにエラーが表示されなかったり、CI/CDパイプラインでだけエラーが出たりすることがあります。
よくある原因
- CI/CD環境との差異
開発環境とCI/CD環境でNode.jsのバージョンや依存関係のインストール状況が異なる。 - パスの問題
ESLintがプロジェクトのルートディレクトリから実行されていない、または間違った設定ファイルを参照している。 - エディタ拡張機能の不具合
エディタのESLint拡張機能が正しく動作していない、または設定が同期されていない。
トラブルシューティング
- 依存関係の再インストール
node_modules
を削除し、npm install
またはyarn install
を再実行して、依存関係が正しくインストールされていることを確認します。 - ターミナルでの直接実行
エディタではなく、ターミナルで直接npx eslint your-file.js
やnpm run lint
を実行して、ESLintが正しくエラーを報告するか確認します。ターミナルでエラーが出て、エディタで出ない場合は、エディタ側の問題です。 - エディタ拡張機能の設定確認
VS Codeの場合、ESLint拡張機能の設定(ESLint: Enable
やESLint: Validate
など)を確認します。 - エディタの再起動
エディタを再起動すると、ESLint拡張機能がリロードされ、問題が解決することがよくあります。
prefer-const
ルールは、JavaScriptの変数宣言において、再代入されない変数をlet
ではなくconst
で宣言することを推奨します。これにより、コードの意図が明確になり、バグを減らすことができます。
ESLintが有効な環境で以下のコードを記述すると、prefer-const
ルールによってどのような警告やエラーが出るか、そしてどのように修正すべきかを見ていきましょう。
基本的な例(letからconstへの変更)
この例では、let
で宣言された変数が一度も再代入されないため、ESLintが警告を出します。
問題のあるコード
// Example 1: Basic re-assignment check
let userName = "Alice";
console.log(userName);
// userName = "Bob"; // この行がコメントアウトされているため、userName は再代入されていない
ESLintの警告/エラー
/path/to/your/file.js
2:5 error 'userName' is never reassigned. Use 'const' instead. prefer-const
修正後のコード
// Example 1: Fixed
const userName = "Alice"; // let から const に変更
console.log(userName);
解説
userName
は初期化後に変更されていないため、const
で宣言するのが適切です。これにより、この変数が以降のコードで変更されないことが保証され、コードの意図が明確になります。
再代入される場合はletでOK
この例では、let
で宣言された変数が実際に再代入されるため、prefer-const
ルールは警告を出しません。
問題ないコード
// Example 2: Variable is re-assigned (prefer-const does not warn)
let counter = 0;
console.log("Initial counter:", counter);
counter = 1; // counter は再代入されている
console.log("Updated counter:", counter);
if (true) {
counter++; // さらに再代入
}
console.log("Final counter:", counter);
ESLintの出力
何も警告やエラーは出ません。
解説
counter
変数は 0
で初期化された後、1
に再代入され、さらにインクリメントされています。このように再代入が伴う場合は、let
を使用するのが正解であり、prefer-const
ルールはこれを正しく判断します。
デストラクチャリングの例
オブジェクトや配列のデストラクチャリングを使用した場合のprefer-const
の挙動です。
問題のあるコード
// Example 3: Destructuring (some variables are not reassigned)
const person = { name: "Bob", age: 25, city: "New York" };
let { name, age, city } = person; // name と city は再代入されない場合
console.log(`${name} is ${age} years old and lives in ${city}.`);
age = 26; // age は再代入される
console.log(`Updated age: ${age}`);
ESLintの警告/エラー
/path/to/your/file.js
3:9 error 'name' is never reassigned. Use 'const' instead. prefer-const
3:24 error 'city' is never reassigned. Use 'const' instead. prefer-const
name
とcity
はlet
で宣言されていますが、再代入されていないため警告が出ます。age
は再代入されているため警告は出ません。
修正後のコード
// Example 3: Fixed Destructuring
const person = { name: "Bob", age: 25, city: "New York" };
// 再代入される age は let に、されない name と city は const に分ける
const { name, city } = person;
let { age } = person;
console.log(`${name} is ${age} years old and lives in ${city}.`);
age = 26;
console.log(`Updated age: ${age}`);
解説
デストラクチャリングの場合でも、個々の変数の再代入の有無に応じてconst
とlet
を使い分けるべきです。これにより、コードの意図がより明確になります。
応用: destructuring
オプション
prefer-const
ルールには destructuring
オプションがあり、デフォルトは "any"
です。これは、デストラクチャリングされた変数の一部でもconst
にできるものがあれば警告するという意味です。
これを "all"
に設定すると、デストラクチャリングされたすべての変数がconst
にできる場合にのみ警告します。
例えば、以下のように設定した場合:
{
"rules": {
"prefer-const": ["error", { "destructuring": "all" }]
}
}
そして、先ほどの問題のあるコードを実行すると、age
が再代入されるため、name
とcity
に対しても警告は出なくなります(すべての変数がconst
にできないため)。このオプションは、一括でlet
にするかconst
にするかを選びたい場合に便利ですが、個々の変数の不変性を強調したい場合はデフォルトの"any"
の方が適しています。
ループ内の変数宣言
ループ内で変数を宣言し、ループの各イテレーションで新しい値が割り当てられる場合でも、prefer-const
は正しく動作します。
問題のあるコード
// Example 4: Loop variable (sometimes can be const)
const numbers = [1, 2, 3];
for (let i = 0; i < numbers.length; i++) {
// i は各イテレーションで再代入されるので let で正しい
let num = numbers[i]; // num はこのスコープ内で再代入されていない
console.log(num);
}
ESLintの警告/エラー
/path/to/your/file.js
4:7 error 'num' is never reassigned. Use 'const' instead. prefer-const
i
はループ変数なので let
で適切ですが、num
はループの各イテレーションで新しい値が代入されるだけで、そのイテレーション内では再代入されていないため、警告が出ます。
修正後のコード
// Example 4: Fixed Loop variable
const numbers = [1, 2, 3];
for (let i = 0; i < numbers.length; i++) {
const num = numbers[i]; // num を const に変更
console.log(num);
}
// または for...of ループを使用する
for (const num of numbers) { // for...of の場合は自然と const になることが多い
console.log(num);
}
解説
ループ内で宣言された変数が、そのループの1回の実行中に再代入されないのであれば、const
を使用すべきです。for...of
ループは、このようなケースでconst
を自然に適用できるため、推奨されることが多いです。
ignoreReadBeforeAssign オプション
このオプションは、変数が宣言された後、初期値が代入される前に参照されている場合に、prefer-const
の警告を無視するものです。これは、複雑な初期化ロジックや特定のデザインパターンで役立ちます。
問題のあるコード(ignoreReadBeforeAssign が false またはデフォルトの場合)
// Example 5: Initializing a variable in multiple steps
let myValue; // ESLintは 'myValue' が再代入されていないと判断する
if (someCondition) {
myValue = "Condition met";
} else {
myValue = "Condition not met";
}
console.log(myValue);
ESLintの警告/エラー
/path/to/your/file.js
2:5 error 'myValue' is never reassigned. Use 'const' instead. prefer-const
myValue
は宣言後に値を割り当てられていますが、ESLintは静的解析で「宣言後の再代入」を検知できず、const
にできると判断します。
設定変更
{
"rules": {
"prefer-const": ["error", { "ignoreReadBeforeAssign": true }]
}
}
修正後のコード(設定変更後、このコードは警告を出しません)
// Example 5: Initializing a variable in multiple steps (with ignoreReadBeforeAssign: true)
let myValue; // ignoreReadBeforeAssign: true の場合、警告が出ない
if (someCondition) {
myValue = "Condition met";
} else {
myValue = "Condition not met";
}
console.log(myValue);
解説
ignoreReadBeforeAssign: true
を設定することで、変数が宣言された直後ではなく、その後の処理で初めて値が割り当てられるパターンを許容します。ただし、このようなケースでも可能であれば三項演算子や即時実行関数などを使ってconst
で初期化することを検討すべきです。
// より良い書き方 (const を使用)
const myValue = someCondition ? "Condition met" : "Condition not met";
console.log(myValue);
はい、ESLintのprefer-const
ルールに関連する「代替手段」というよりは、prefer-const
ルールがあることによって推奨されるプログラミングの代替手段(書き換え方)、またはprefer-const
ルールを適用できない・適用したくない場合の代替策としてご説明します。
prefer-const
ルールは、「再代入されない変数はconst
で宣言すべき」という原則を強制します。この原則に沿うために、コードをよりconst
を使いやすく書き換える「代替手段」と、どうしてもlet
を使いたい場合の「代替策」があります。
let を const に書き換えるための代替手段
これがprefer-const
ルールの最も直接的な目的であり、推奨される「代替手段」です。
a. 変数を宣言時に初期化する
多くの場合は、変数の宣言と初期化を同時に行うことで、let
ではなくconst
を使用できます。
prefer-constの警告が出る例
// let で宣言したが、後で値を代入し、それ以降変更されない
let message;
if (condition) {
message = "Hello";
} else {
message = "Goodbye";
}
console.log(message);
代替手段(constを使用する書き換え)
三項演算子や即時実行関数(IIFE: Immediately Invoked Function Expression)を使って、宣言と同時に初期化します。
// 三項演算子で書き換え
const message = condition ? "Hello" : "Goodbye";
console.log(message);
// IIFE で書き換え
const anotherMessage = (() => {
if (someOtherCondition) {
return "Alpha";
} else {
return "Beta";
}
})();
console.log(anotherMessage);
メリット
prefer-const
ルールに準拠します。- コードの可読性が向上し、意図しない再代入によるバグを防ぎます。
- 変数の値が一度設定されたら変更されないことが明確になります。
b. デストラクチャリングの分割
オブジェクトや配列のデストラクチャリングで、一部の変数だけが再代入される場合、それらを分割してconst
とlet
を使い分けます。
prefer-constの警告が出る例
let { id, name, status } = someData; // id と name は再代入されない
status = "active"; // status だけ再代入される
console.log(id, name, status);
代替手段(constとletを使い分ける)
const { id, name } = someData; // 再代入されないものは const
let { status } = someData; // 再代入されるものだけ let
status = "active";
console.log(id, name, status);
メリット
prefer-const
ルールに準拠します。- 変数の不変性が明確になり、それぞれの変数の役割がはっきりします。
c. for...of
ループの使用
for
ループでインデックス変数と要素変数をlet
で宣言し、要素変数がループ内で再代入されない場合、for...of
ループに書き換えることでconst
を自然に適用できます。
prefer-constの警告が出る例
const numbers = [1, 2, 3];
for (let i = 0; i < numbers.length; i++) {
let num = numbers[i]; // num はこのスコープ内で再代入されない
console.log(num);
}
代替手段(for...ofループに書き換え)
const numbers = [1, 2, 3];
for (const num of numbers) { // num は各イテレーションで新しい定数として扱われる
console.log(num);
}
メリット
prefer-const
ルールに準拠します。- 各イテレーションで
num
がconst
として扱われるため、意図しない変更を防ぎます。 - より簡潔で可読性の高いコードになります。
prefer-constルールを適用できない・適用したくない場合の代替策
prefer-const
ルールは非常に強力ですが、すべてのケースで完璧にフィットするわけではありません。以下は、やむを得ずlet
を使用する場合の代替策です。
a. ESLintのコメントでルールを無効化する
一時的、または特定の理由でprefer-const
ルールを適用したくない場合に、コード内にESLintのコメントを追加してルールを無効化できます。
使用例
let value; // 初期化が複雑で、宣言時に値を決定できない場合など
// ... 複雑なロジック
if (condition1) {
value = "Option A";
} else if (condition2) {
value = "Option B";
} else {
// eslint-disable-next-line prefer-const
value = "Default Option"; // ここで prefer-const の警告を無視
}
console.log(value);
メリット
- コードベース全体の設定を変更せずに対応できます。
- 特定の行やブロックでルールを一時的に無効にできます。
デメリット
- コメントによる無効化は、なぜ
let
であるべきなのかの理由を明確にするコメントと一緒に使うべきです。 - 乱用すると、Linterの本来の目的(コード品質の維持)を損なう可能性があります。
b. prefer-const
ルールのオプションを調整する
ESLintの設定ファイルで、prefer-const
ルールの振る舞いを調整するオプションがあります。
-
ignoreReadBeforeAssign オプション
true
: 変数が宣言された後、初期値が代入される前に参照されている場合、prefer-const
の警告を無視します。- 設定例
{ "rules": { "prefer-const": ["error", { "ignoreReadBeforeAssign": true }] } }
- 用途
宣言と最初の代入の間に複雑なロジックを挟む必要がある場合など、const
に書き換えにくいパターンで警告を抑制したい場合に利用します。
-
"any"
(デフォルト): デストラクチャリングされた変数の一部でもconst
にできるものがあれば警告。"all"
: デストラクチャリングされたすべての変数がconst
にできる場合にのみ警告。- 設定例
{ "rules": { "prefer-const": ["error", { "destructuring": "all" }] } }
- 用途
デストラクチャリングで一部だけlet
にする場合でも、残りのconst
にできる変数に対して警告を出したくない場合に利用します。
メリット
- プロジェクト全体でルールの挙動を微調整できます。
デメリット
- ルールの意図を完全に無効化する可能性があるので、慎重に検討する必要があります。
- プロジェクトのすべての開発者に影響を与えます。
c. ルールの重要度を下げる、または無効化する(非推奨)
最終手段として、prefer-const
ルールの重要度を"warn"
に下げるか、"off"
にして完全に無効化する方法もあります。
設定例
{
"rules": {
"prefer-const": "warn" // エラーではなく警告にする
// "prefer-const": "off" // ルールを完全に無効にする
}
}
メリット
- 一時的に問題を回避できます。
デメリット
- 強く非推奨です。 コード品質を低下させ、将来的なバグの原因となる可能性があります。特別な理由がない限り、この方法は避けるべきです。