クリーンなコードへ:ESLint「no-shadow」の代替アプローチと設定例

2025-05-27

シャドーイングとは何か?

シャドーイングとは、あるスコープ(ブロックや関数など)で宣言された変数が、その外側のスコープで既に宣言されている同じ名前の変数を「覆い隠す」ことを指します。これにより、外側のスコープの変数にアクセスできなくなり、意図しない挙動やバグの原因となる可能性があります。

簡単な例で見てみましょう。

// 外側のスコープの 'a'
let a = 10; 

function foo() {
  // 内側のスコープの 'a'。これは外側の 'a' をシャドーイングしています。
  let a = 20; 
  console.log(a); // 20が出力される
}

foo();
console.log(a); // 10が出力される

この例では、foo 関数内で宣言された a が、グローバルスコープで宣言された a をシャドーイングしています。foo 関数内では、内側の a が優先されるため、console.log(a)20 を出力します。しかし、foo 関数の外では、依然としてグローバルスコープの a が参照されるため、console.log(a)10 を出力します。

「no-shadow」ルールの目的

「no-shadow」ルールは、このようなシャドーイングを検出して警告またはエラーとして報告することで、以下のような問題を防ぐことを目的としています。

  1. コードの可読性の低下
    同じ名前の変数が複数のスコープに存在すると、どちらの変数が参照されているのかが分かりにくくなり、コードの理解を妨げます。
  2. 意図しないバグの発生
    外側の変数を参照しているつもりだったが、実際には内側の同名変数を参照してしまっていた、というようなバグが発生する可能性があります。特に、複雑なネストされたコードでは、この種のバグを見つけるのが困難になります。
  3. デバッグの困難さ
    シャドーイングが発生していると、変数の値がどこでどのように変化しているのかを追跡するのが難しくなります。

「no-shadow」ルールには、いくつかのオプションがあり、特定の状況でシャドーイングを許可したり、より厳密にチェックしたりすることができます。主なオプションは以下の通りです。

  • allow: シャドーイングを許可する識別子(変数名)のリストを指定できます。例えば、非同期処理のコールバックで慣例的に同じ名前の引数(例: donecallback)を使う場合などに利用されます。
  • hoist: 変数や関数の巻き上げ(hoisting)の挙動に関連して、シャドーイングをいつ報告するかを制御します。「functions」(デフォルト)、「all」、「never」の3つの設定があります。
  • builtinGlobals: true に設定すると、ObjectArray といった組み込みのグローバル変数もシャドーイングの対象としてチェックします。通常は false です。


ここでは、no-shadowルールに関連するよくあるエラーと、そのトラブルシューティング方法を説明します。

よくあるエラーとその原因

  1. function processData(data) {
      // 'data' は引数であり、内部で同じ名前の変数を宣言するとシャドーイングになる
      const data = parse(data); // ESLint: 'data' is already declared in the upper scope. (no-shadow)
      // ...
    }
    
    • 原因: 関数やメソッドの引数名と、その関数/メソッド内部で宣言する変数名が同じである場合に発生します。これは典型的なシャドーイングの例です。
  2. catch ブロック内のエラー変数

    try {
      // ...
    } catch (error) {
      // 'error' は既にcatchブロックのスコープで宣言されている
      const error = new CustomError('Something went wrong'); // ESLint: 'error' is already declared in the upper scope. (no-shadow)
      // ...
    }
    
    • 原因: catchブロックの引数としてerrorが宣言されているにもかかわらず、ブロック内で再度errorという名前の変数を宣言した場合に発生します。
  3. ループ変数と外部変数のシャドーイング

    let i = 0;
    for (let i = 0; i < 5; i++) { // ESLint: 'i' is already declared in the upper scope. (no-shadow)
      console.log(i);
    }
    
    • 原因: 外側のスコープで既に宣言されているループ変数(例: i)を、内側のループでletconstを使って再宣言した場合に発生します。
  4. Promiseチェーンやコールバック関数の引数

    fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        // 外部スコープに 'data' が存在する場合に発生しうる
        const data = process(data); // ESLint: 'data' is already declared in the upper scope. (no-shadow)
        // ...
      });
    
    • 原因: Promiseチェーンやコールバック関数で、引数として受け取った変数と同じ名前の変数を内部で宣言した場合に発生します。特に、引数の名前が一般的(data, err, resなど)な場合によく見られます。
  5. TypeScriptにおけるEnumやInterfaceとの名前衝突

    enum Status {
      Active,
      Inactive,
    }
    
    function getStatus() {
      const Status = 'active'; // ESLint: 'Status' is already declared in the upper scope. (no-shadow)
      return Status;
    }
    
    • 原因: TypeScriptの場合、enuminterfaceの名前が、実行時に使用される変数名と衝突することがあります。ESLintはこれらも「宣言」と見なすため、シャドーイングとして検出されることがあります。@typescript-eslint/eslint-pluginを使用している場合に特に注意が必要です。
  6. builtinGlobalsオプションによる組み込みグローバルオブジェクトのシャドーイング ESLintのno-shadowルールにはbuiltinGlobalsというオプションがあり、これがtrueに設定されていると、ObjectArrayNumberなどのJavaScript組み込みのグローバルオブジェクトの名前をシャドーイングすることも禁止されます。

    // .eslintrc.js
    // rules: {
    //   "no-shadow": ["error", { "builtinGlobals": true }]
    // }
    
    function MyObject() {
      const Object = {}; // ESLint: 'Object' is already declared in the upper scope. (no-shadow)
      // ...
    }
    
    • 原因: 組み込みグローバルオブジェクト(ObjectArrayなど)と同じ名前の変数を宣言した場合に発生します。通常、このオプションはfalse(デフォルト)なので、あまり見かけないかもしれませんが、厳格な設定のプロジェクトでは発生しえます。
  1. 変数名の変更 最も直接的で推奨される解決策は、シャドーイングしている変数の名前を変更することです。

    // 悪い例
    // function processData(data) {
    //   const data = parse(data);
    // }
    
    // 良い例
    function processData(rawData) { // 引数名を変更
      const parsedData = parse(rawData); // 内部変数名を変更
      // ...
    }
    

    これにより、コードの可読性も向上し、意図しない挙動も防げます。

  2. catch ブロックの変数名を変更 catch ブロックの変数を再宣言しないようにします。

    try {
      // ...
    } catch (originalError) { // 引数名を変更
      const newError = new CustomError('Something went wrong with ' + originalError.message);
      // ...
    }
    

    または、catchブロック内でerror変数を再利用しないようにします。

  3. no-shadow ルールのオプション調整 プロジェクトの要件やコーディングスタイルによっては、一部のシャドーイングを許容したい場合があります。その場合、.eslintrc.jsなどの設定ファイルでno-shadowルールのオプションを調整します。

    • allow オプション: 特定の変数名のみシャドーイングを許可する場合に使用します。例えば、ReactのcontextやReduxのmapDispatchToPropsなどで、引数と内部変数が同じ名前になることが慣例的に許容される場合に便利です。

      // .eslintrc.js
      // rules: {
      //   "no-shadow": ["error", { "allow": ["data", "error", "props"] }]
      // }
      
      // 例: 'data' のシャドーイングを許可
      fetch('/api/data')
        .then(response => response.json())
        .then(data => {
          const data = process(data); // これでエラーにならない
          // ...
        });
      

      ただし、allowを多用すると、no-shadowルール本来の目的である「意図しないバグの防止」が損なわれる可能性があるため、慎重に検討する必要があります。

    • builtinGlobals オプション: 組み込みグローバルオブジェクトのシャドーイングを許可する場合は、このオプションをfalseに設定します(デフォルトはfalseですが、明示的に指定する場合)。

      // .eslintrc.js
      // rules: {
      //   "no-shadow": ["error", { "builtinGlobals": false }]
      // }
      
  4. 一時的な無効化 (非推奨) ごく稀に、どうしても解決策が見つからない、または一時的に特定の行のエラーを無視したい場合に、コメントでルールを無効化する方法もあります。

    // eslint-disable-next-line no-shadow
    const data = parse(data); // この行のno-shadowルールを無視
    

    これは一時的な解決策であり、根本的な問題解決ではないため、乱用は避けるべきです。

no-shadowルールは、コードの品質と保守性を高めるために非常に役立つルールです。エラーが発生した場合は、安易にルールを無効化するのではなく、まず変数名を変更するなど、コードの明確性を向上させる方向で解決を試みることをお勧めします。



no-shadowルールとは?

no-shadowルールは、JavaScriptのスコープ内で変数が「シャドーイング(shadowing)」されることを防ぐためのESLintルールです。シャドーイングとは、内側のスコープで宣言された変数が、外側のスコープで同じ名前で宣言された変数を「覆い隠してしまう」現象を指します。これにより、コードの可読性が低下したり、意図しないバグが発生する可能性があります。

例1: 関数引数とローカル変数のシャドーイング

最もよくあるケースです。関数の引数と同じ名前の変数を関数内で再宣言すると発生します。

悪い例 (ESLintがエラーを報告)

// main.js
function processData(data) {
  // 'data' は既に引数として宣言されています。
  // ここで const data = ... とすると、外側の data を覆い隠します。
  const data = JSON.parse(data); // <-- ESLint: 'data' is already declared in the upper scope. (no-shadow)
  console.log(data); // これはJSON.parseされた data を参照します
}

processData('{ "name": "Alice" }');

修正例 (変数名を変更)

// main.js
function processData(rawData) { // 引数名を 'rawData' に変更
  const parsedData = JSON.parse(rawData); // 内部変数名を 'parsedData' に変更
  console.log(parsedData);
}

processData('{ "name": "Alice" }');

解説: 引数と内部変数の名前を明確に分けることで、どちらのdataを参照しているのかが明確になり、可読性が向上します。

例2: catchブロックでのエラー変数シャドーイング

try...catch文のcatchブロックで、エラーを捕捉する引数と同じ名前の変数を宣言すると発生します。

悪い例 (ESLintがエラーを報告)

// main.js
try {
  throw new Error('Something went wrong!');
} catch (error) { // 'error' は既にcatchブロックの引数として宣言されています
  const error = new Error('Custom error message'); // <-- ESLint: 'error' is already declared in the upper scope. (no-shadow)
  console.error(error.message); // これは 'Custom error message' を表示します
}

修正例 (変数名を変更)

// main.js
try {
  throw new Error('Something went wrong!');
} catch (originalError) { // 引数名を 'originalError' に変更
  const customError = new Error('Custom error message based on: ' + originalError.message);
  console.error(customError.message);
}

解説: catchブロックの引数と、その中で使う変数の名前を区別することで、混乱を防ぎます。

例3: ループ変数と外部変数のシャドーイング

ループ処理で、外側のスコープにある変数と同じ名前のループ変数を宣言すると発生します。

悪い例 (ESLintがエラーを報告)

// main.js
let i = 100; // 外側のスコープの 'i'

for (let i = 0; i < 5; i++) { // <-- ESLint: 'i' is already declared in the upper scope. (no-shadow)
  console.log('Loop i:', i); // これはループ内の 'i' (0, 1, 2, 3, 4)
}

console.log('Outside i:', i); // これは外側の 'i' (100)

修正例 (変数名を変更)

// main.js
let globalCounter = 100; // 外側のスコープの変数を別の名前に変更

for (let i = 0; i < 5; i++) { // ループ変数は 'i' のままでOK
  console.log('Loop i:', i);
}

console.log('Outside counter:', globalCounter);

解説: このケースでは、外側の変数とループ変数が意図的に異なる目的で使用されている可能性があります。その場合は、それぞれの変数名を明確に区別することが重要です。

例4: Promiseチェインやコールバック関数の引数シャドーイング

悪い例 (ESLintがエラーを報告)

// main.js
function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => resolve({ name: 'Bob', age: 30 }), 100);
  });
}

fetchData()
  .then(data => { // Promiseから受け取った 'data'
    // ここでまた 'data' という名前で変数を宣言するとシャドーイング
    const data = { ...data, status: 'active' }; // <-- ESLint: 'data' is already declared in the upper scope. (no-shadow)
    console.log(data); // これは { name: 'Bob', age: 30, status: 'active' }
  });

修正例 (変数名を変更)

// main.js
function fetchData() {
  return new Promise(resolve => {
    setTimeout(() => resolve({ name: 'Bob', age: 30 }), 100);
  });
}

fetchData()
  .then(originalData => { // 引数名を 'originalData' に変更
    const processedData = { ...originalData, status: 'active' }; // 内部変数名を 'processedData' に変更
    console.log(processedData);
  });

解説: 特に非同期処理では、前のステップで受け取ったデータと、それを加工した後のデータを区別するために、明確な変数名を使用することが推奨されます。

.eslintrc.jsなどのESLint設定ファイルで、no-shadowルールにオプションを設定することで、一部のシャドーイングを許可することも可能です。

allowオプションの使用例

特定の変数名についてはシャドーイングを許可したい場合にallowオプションを使用します。

// .eslintrc.js
module.exports = {
  rules: {
    'no-shadow': ['error', { 'allow': ['done', 'cb', 'callback'] }]
  }
};

allowオプション適用後のコード例 (ESLintはエラーを報告しない)

// main.js
// done, cb, callback は allow オプションで許可されていると仮定
function asyncOperation(done) {
  // 'done' は引数
  const done = () => console.log('Operation completed!'); // <-- ESLintはエラーを報告しない
  setTimeout(done, 1000);
}

asyncOperation(() => {});

注意: allowオプションは、特定の慣習(例えばNode.jsのコールバックにおけるcallbackcbなど)に合わせて使用すべきであり、安易に多用するとno-shadowルールが提供する恩恵が失われる可能性があります。可能な限り、変数名を変更してシャドーイングを回避することが推奨されます。



ここでは、no-shadowに関連する問題に対するプログラミングにおける代替手段を、大きく3つのカテゴリに分けて説明します。

変数名を変更する (最も推奨されるアプローチ)

これが最も根本的かつ推奨される解決策です。シャドーイングが発生している場合、それは多くの場合、変数名が不明瞭であるか、同じ名前の変数が異なる目的で使用されていることを示しています。変数名を変更することで、コードの意図が明確になり、可読性と保守性が向上します。


// 問題のあるコード (no-shadow エラー)
function processUser(user) {
  const user = { ...user, status: 'active' }; // 引数 'user' をシャドーイング
  console.log(user);
}

// 修正後 (変数名を変更)
function processUser(rawUser) { // 引数名を変更
  const processedUser = { ...rawUser, status: 'active' }; // 内部変数を変更
  console.log(processedUser);
}

利点

  • ESLintの設定をいじる必要がないため、最もクリーンな解決策。
  • シャドーイングによる意図しないバグを防ぐ。
  • コードの可読性が大幅に向上する。

いつ使うか

  • 特に、引数と内部変数が異なる意味を持つ場合に有効。
  • ほぼ全てのシャドーイングのケースで適用可能。

スコープを適切に設計する / ブロックスコープを活用する

シャドーイングはスコープの設計が原因で発生することが多いです。変数の生存期間やアクセス範囲を明確にすることで、シャドーイングを回避できます。特にES6以降のletconstを活用したブロックスコープは、この問題の解決に役立ちます。

例 (ループ変数のシャドーイング)

// 問題のあるコード (no-shadow エラー)
let count = 0;
for (let count = 0; count < 5; count++) { // 外側の 'count' をシャドーイング
  console.log(count);
}

// 修正後 (ループ変数を別の名前にする)
let globalCount = 0; // 外側の変数を別の名前に
for (let i = 0; i < 5; i++) { // ループ変数は一般的な 'i' でOK
  console.log(i);
}

// 別解 (外側の変数をループ内で使用しない場合、問題にならないことも)
function doSomething() {
  let count = 0; // 関数スコープの 'count'
  for (let count = 0; count < 5; count++) { // ブロックスコープの 'count'。関数スコープの 'count' をシャドーイング。
    // この場合はno-shadowエラーになることが多い
    console.log(count);
  }
}

利点

  • 意図しない副作用を避けることができる。
  • 変数のスコープが明確になり、コードの論理構造が理解しやすくなる。

いつ使うか

  • 大規模な関数や複雑なロジックにおいて、変数の役割を明確にしたい場合。
  • 特に、ループ変数や一時的なブロックスコープ内で宣言される変数が、外側の変数と衝突する場合。

上記2つの方法が適用できない、または特定の慣習に従う必要がある場合に、ESLintのno-shadowルールの設定自体を調整するという選択肢があります。ただし、これは潜在的なバグを見逃す可能性を高めるため、慎重に検討する必要があります。

3-1. allowオプションで特定の名前を許可する

特定の変数名についてのみシャドーイングを許可する場合にallowオプションを使用します。これは、フレームワークやライブラリの特定のパターン(例: React Contextのvalue、Node.jsのコールバックにおけるcallbackなど)で、同名の引数と内部変数が慣例的に使われる場合に有効です。

.eslintrc.js の設定例

module.exports = {
  rules: {
    'no-shadow': ['error', { 'allow': ['callback', 'done', 'err', 'res'] }]
  }
};

コード例 (ESLintはエラーを報告しない)

function processRequest(req, res) {
  // 'res' は引数として宣言されていますが、allowリストにあるためOK
  const res = { ...res, statusCode: 200 }; // ESLintはエラーを報告しない
  sendResponse(res);
}

function handleAsync(callback) {
  // 'callback' は引数として宣言されていますが、allowリストにあるためOK
  const callback = () => console.log('Finished!'); // ESLintはエラーを報告しない
  setTimeout(callback, 100);
}

利点

  • 特定のコーディング規約やライブラリのパターンに合わせた柔軟な対応が可能。

欠点

  • リストが増えすぎると管理が煩雑になる。
  • allowリストに追加された名前については、本来検出されるべきシャドーイングが見過ごされる可能性がある。

いつ使うか

  • 既存のコードベースが非常に大きく、変数名の変更が非現実的な場合(ただし、推奨はしない)。
  • プロジェクト全体で広く使われている、シャドーイングを伴う特定の命名慣習がある場合。

3-2. TypeScript固有のESLintルールを使用する (@typescript-eslint/no-shadow)

TypeScriptプロジェクトの場合、@typescript-eslint/eslint-pluginに含まれる@typescript-eslint/no-shadowルールを使用することが推奨されます。このルールは、TypeScriptのEnumやInterfaceなど、JavaScriptのno-shadowルールでは考慮されないTypeScript固有の型定義を考慮してシャドーイングを検出します。

.eslintrc.js の設定例

module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
  ],
  rules: {
    'no-shadow': 'off', // 元のno-shadowルールを無効化
    '@typescript-eslint/no-shadow': 'error', // TypeScript用のno-shadowルールを有効化
  }
};

利点

  • 型定義と変数名の衝突など、TypeScript固有の問題に対応できる。
  • TypeScriptの言語特性に合わせた正確なシャドーイング検出。

いつ使うか

  • TypeScriptを使用しているプロジェクト。

3-3. ルールを一時的に無効化する (最終手段、非推奨)

特定の行やブロックで、どうしてもシャドーイングを回避できない、または一時的にESLintの警告を抑制したい場合に、コメントを使ってルールを無効化する方法があります。これは最終手段であり、可能な限り避けるべきです。

// eslint-disable-next-line no-shadow
const data = parse(data); // この行のみ no-shadow ルールを無視

function someFunction(value) {
  // eslint-disable-next-line no-shadow
  if (true) {
    const value = 10; // このブロック内の no-shadow ルールを無視
    console.log(value);
  }
}

利点

  • 緊急時や一時的な回避策として使える。

欠点

  • レビューで指摘されやすい。
  • no-shadowルールの目的を損なう。
  • 問題を根本的に解決せず、コードの潜在的なバグや可読性の問題を隠蔽してしまう。

いつ使うか

  • 本番コードでは極力避けるべき。
  • 短期間のプロトタイピングや、外部ライブラリとの連携でどうしても回避できない特定の状況(ただし、その場合でもコメントで理由を明記すべき)。

ESLintのno-shadowルールに関連するプログラミングの代替手段は、問題の深刻度とプロジェクトの要件によって異なります。

  1. 変数名の変更(最も推奨): ほぼ常に最善の解決策。
  2. スコープの適切な設計: コードの構造を改善し、自然にシャドーイングを避ける。
  3. ESLint設定の調整:
    • allowオプション: 特定の慣習に対応。
    • @typescript-eslint/no-shadow: TypeScriptプロジェクトで必須。
    • 一時的な無効化: 最終手段であり、極力避ける。