no-param-reassignの壁を越える!ESLint準拠のプログラミング手法

2025-05-31

no-param-reassign は、ESLint のルールの一つで、関数の引数(パラメータ)への再代入を禁止するためのものです。

なぜこのルールがあるのか?

JavaScript では、関数の引数は、プリミティブ型(数値、文字列、真偽値など)の場合は値渡し、オブジェクト型(配列、オブジェクトなど)の場合は参照渡しになります。

  1. 予期せぬ副作用を防ぐため:

    • 値渡しの場合: 引数に再代入しても、関数の外にある元の変数の値は変わりません。しかし、コードを読んだ人が「元の値が変わるかも」と誤解する可能性があります。
    • 参照渡しの場合: 引数に再代入すると、その引数が別のオブジェクトを参照するようになります。これは一見問題ないように見えますが、もし関数の内部で引数のプロパティを変更するような操作も行われている場合、元のオブジェクトまで変更されてしまうことがあります。これは「副作用」と呼ばれ、コードの予測可能性を低下させ、バグの原因となることがあります。
    • 特に、関数が純粋な関数(pure function)であるべき場所(例えば、React のコンポーネントの props や Redux の reducer など)で引数を変更すると、デバッグが非常に困難になることがあります。
  2. コードの可読性と保守性の向上:

    • 引数への再代入は、関数の意図を不明瞭にすることがあります。関数は通常、引数を受け取り、それらを使って新しい値を計算したり、特定の処理を実行したりするものですが、引数を直接変更することは、その関数の役割を曖昧にしてしまいます。
    • 新しい変数を宣言してそこに値をコピーし、そのコピーを操作する方が、コードの意図が明確になり、後からコードを読んだ人にとっても理解しやすくなります。

具体的な例

悪い例 (no-param-reassign が警告を出す例)

function processData(data) {
  // data に再代入しているため、no-param-reassign が警告を出す
  data = data.toUpperCase(); 
  console.log(data);
}

const myString = "hello";
processData(myString); // 出力: HELLO
console.log(myString); // 出力: hello (myString自体は変わらない)

function modifyObject(obj) {
  // obj に再代入しているため、no-param-reassign が警告を出す
  obj = { a: 1, b: 2 }; 
  console.log(obj);
}

const myObject = { x: 10 };
modifyObject(myObject); // 出力: { a: 1, b: 2 }
console.log(myObject); // 出力: { x: 10 } (myObject自体は変わらない)

function appendToArray(arr) {
  // 配列のプロパティを直接変更している場合 (オプションによっては警告の対象)
  // デフォルトでは警告しないが、オプション "props": true を設定すると警告
  arr.push(4); 
  console.log(arr);
}

const myArray = [1, 2, 3];
appendToArray(myArray); // 出力: [1, 2, 3, 4]
console.log(myArray); // 出力: [1, 2, 3, 4] (myArray自体が変更される)

良い例 (no-param-reassign に違反しない例)

function processData(data) {
  // 新しい変数にコピーして操作する
  const processedData = data.toUpperCase(); 
  console.log(processedData);
}

const myString = "hello";
processData(myString); // 出力: HELLO
console.log(myString); // 出力: hello

function modifyObject(obj) {
  // 新しいオブジェクトを作成して返す
  const newObj = { ...obj, a: 1, b: 2 }; 
  console.log(newObj);
}

const myObject = { x: 10 };
modifyObject(myObject); // 出力: { x: 10, a: 1, b: 2 }
console.log(myObject); // 出力: { x: 10 }

function appendToArray(arr) {
  // 新しい配列を作成して返す
  const newArr = [...arr, 4]; 
  console.log(newArr);
  return newArr; // 必要であれば新しい配列を返す
}

const myArray = [1, 2, 3];
const updatedArray = appendToArray(myArray); // 出力: [1, 2, 3, 4]
console.log(myArray); // 出力: [1, 2, 3] (myArray自体は変わらない)
console.log(updatedArray); // 出力: [1, 2, 3, 4]

ルールのオプション

no-param-reassign には、いくつかのオプションがあります。

  • ignorePropertyModificationsFor: [] (デフォルト)

    • props: true の場合に有効です。この配列に指定された引数名については、プロパティの変更を許可します。例えば、Array.prototype.reduce のアキュムレータ (acc) のように、意図的に引数のプロパティを変更するケースがある場合に利用します。
  • props: false (デフォルト)

    • false の場合: 引数への再代入 (param = newValue) のみを禁止します。
    • true の場合: 引数のプロパティへの変更 (param.property = newValuedelete param.property など) も禁止します。これは、オブジェクトの参照渡しによる副作用をより厳密に防ぎたい場合に有用です。

いつこのルールを無効にするか?

通常、no-param-reassign は有効にしておくことが推奨されます。しかし、以下のような特定の状況では、このルールを一時的に無効にすることも検討できます。

  • 特定のライブラリやパターン: Array.prototype.reduce のアキュムレータのように、引数を意図的に変更するデザインパターンが存在する場合。この場合は、ignorePropertyModificationsFor オプションを使う方が望ましいですが、複雑な場合はルールを無効にすることもあります。
  • パフォーマンス上の理由: 非常に大きなデータ構造を扱う場合など、新しいオブジェクトを作成するコストが高い場合に、引数を直接変更する方がパフォーマンスが良い場合があります。ただし、この場合でも、その変更が副作用を伴わないように注意深く設計する必要があります。


no-param-reassign は、コードの健全性を保つ上で非常に有用なルールですが、特に JavaScript の「値渡し」と「参照渡し」の挙動を完全に理解していない場合や、特定のプログラミングパターンに慣れていない場合に、混乱やエラーの原因となることがあります。

エラーメッセージの理解

まず、ESLint が出力するエラーメッセージを確認しましょう。通常は以下のような形式で表示されます。

[eslint] Do not reassign function parameters. (no-param-reassign)
[eslint] Do not modify properties of function parameters. (no-param-reassign)
  • Do not modify properties of function parameters.: これは、props: true オプションが有効になっている場合に、オブジェクトや配列の引数のプロパティを変更しようとした場合に発生します。 例: function foo(obj) { obj.prop = 'value'; }
  • Do not reassign function parameters.: これは、関数の引数自体に新しい値を再代入しようとした場合に発生します。 例: function foo(bar) { bar = 1; }

よくあるエラーパターンと解決策

エラーパターン 1: プリミティブ型引数の再代入
// bad
function increment(num) {
  num = num + 1; // ここで no-param-reassign エラー
  return num;
}

問題点: プリミティブ型の場合、引数に再代入しても呼び出し元の変数は変更されませんが、ルールによって禁止されています。

解決策: 新しいローカル変数を宣言して、その変数を操作します。

// good
function increment(num) {
  const newNum = num + 1; // 新しい変数に結果を格納
  return newNum;
}

// または、元の値をそのまま返す
function increment(num) {
  return num + 1;
}
エラーパターン 2: オブジェクト型引数(オブジェクト)の再代入
// bad
function updateConfig(config) {
  config = { ...config, enableFeature: true }; // ここで no-param-reassign エラー
  return config;
}

問題点: オブジェクトのプロパティを更新するために、引数自体に新しいオブジェクトを再代入しようとしています。これは参照渡しであるオブジェクトの場合でも禁止されます。

解決策: 新しいオブジェクトを作成して返します。スプレッド構文 (...) や Object.assign() を使って、元のオブジェクトを変更せずに新しいオブジェクトを作成するのが一般的です。

// good
function updateConfig(config) {
  // スプレッド構文で新しいオブジェクトを作成
  const newConfig = { ...config, enableFeature: true }; 
  return newConfig;
}

// または Object.assign() を使う
function updateConfig(config) {
  const newConfig = Object.assign({}, config, { enableFeature: true });
  return newConfig;
}

// 使用例
const myConfig = { theme: 'dark', enableFeature: false };
const updatedConfig = updateConfig(myConfig);
console.log(myConfig);      // { theme: 'dark', enableFeature: false } (変更なし)
console.log(updatedConfig); // { theme: 'dark', enableFeature: true }
エラーパターン 3: オブジェクト型引数(オブジェクト)のプロパティ変更 (props: true の場合)
// .eslintrc.js で "props": true が設定されている場合
// "no-param-reassign": ["error", { "props": true }]

// bad
function setDefaultValues(options) {
  if (options.timeout === undefined) {
    options.timeout = 5000; // ここで no-param-reassign エラー
  }
  return options;
}

問題点: オブジェクトの引数のプロパティを直接変更しようとしています。props: true が設定されている場合、これは副作用と見なされ禁止されます。

解決策: 新しいオブジェクトを生成し、必要な変更を加えて返します。

// good
function setDefaultValues(options) {
  const newOptions = { ...options }; // まずコピーを作成
  if (newOptions.timeout === undefined) {
    newOptions.timeout = 5000; // コピーのプロパティを変更
  }
  return newOptions;
}

// または、初期値を設定する際にデフォルトパラメータとオブジェクトの結合を活用
function processOptions(options = { timeout: 5000 }) {
  // 必要であれば、ここでさらにオプションを調整するための新しいオブジェクトを作成
  const finalOptions = { timeout: 5000, ...options }; // optionsが優先される
  return finalOptions;
}
エラーパターン 4: 配列型引数の変更 (props: true の場合)
// .eslintrc.js で "props": true が設定されている場合

// bad
function addElement(arr, element) {
  arr.push(element); // ここで no-param-reassign エラー
  return arr;
}

問題点: 配列の push()splice() など、配列を直接変更するメソッドを使用しています。これも props: true の場合に禁止されます。

解決策: 新しい配列を生成し、元の配列を変更せずに要素を追加します。スプレッド構文 (...) や Array.prototype.concat() が役立ちます。

// good
function addElement(arr, element) {
  const newArr = [...arr, element]; // スプレッド構文で新しい配列を作成
  return newArr;
}

// または concat() を使う
function addElement(arr, element) {
  const newArr = arr.concat(element);
  return newArr;
}

// 使用例
const myNumbers = [1, 2, 3];
const updatedNumbers = addElement(myNumbers, 4);
console.log(myNumbers);      // [1, 2, 3] (変更なし)
console.log(updatedNumbers); // [1, 2, 3, 4]
エラーパターン 5: Array.prototype.reduce() のアキュムレータ

reduce メソッドのコールバック関数内でアキュムレータを直接変更する場合、props: true オプションが有効だとエラーになることがあります。

// .eslintrc.js で "props": true が設定されている場合

// bad
const data = [{ id: 'a', value: 1 }, { id: 'b', value: 2 }];
const result = data.reduce((acc, item) => {
  acc[item.id] = item.value; // ここで no-param-reassign エラー
  return acc;
}, {});

問題点: reduce の一般的な使い方であり、アキュムレータの変更は reduce 操作全体としては副作用ではないと見なされることが多いですが、ルールはこれを検出します。

解決策:

  1. ignorePropertyModificationsFor オプションを使用する: ESLint の設定で、特定の引数名(この場合は acc)のプロパティ変更を無視するように設定します。

    // .eslintrc.js
    {
      "rules": {
        "no-param-reassign": ["error", { 
          "props": true,
          "ignorePropertyModificationsFor": ["acc"] // 'acc' という名前の引数のプロパティ変更を許可
        }]
      }
    }
    
  2. 新しいオブジェクトを返すようにする(より純粋なアプローチ): 各イテレーションで新しいオブジェクトを返すようにします。これは、アキュムレータが常に新しいオブジェクトになるため、より純粋な関数型プログラミングのアプローチです。ただし、大規模なデータセットではパフォーマンスに影響が出る可能性もあります。

    // good (より純粋なアプローチ)
    const data = [{ id: 'a', value: 1 }, { id: 'b', value: 2 }];
    const result = data.reduce((acc, item) => {
      return { ...acc, [item.id]: item.value }; // 新しいオブジェクトを返す
    }, {});
    console.log(result); // { a: 1, b: 2 }
    
  • 既存のコードベースへの導入: 大規模な既存のプロジェクトに no-param-reassign を導入する場合、大量のエラーが発生する可能性があります。その場合は、一度に全てを修正するのではなく、徐々に修正を進めるか、ルールの深刻度を warn に設定して、将来のコードから適用していくなどの戦略を検討してください。

  • リファクタリングの検討: このエラーは、関数の設計が「副作用」を許容していることを示している場合があります。関数の引数を変更する代わりに、新しい値を返すようにリファクタリングすることで、コードの意図が明確になり、バグの発生を抑えることができます。

  • ESLint の設定を確認する: no-param-reassign ルールがプロジェクトの .eslintrc.jspackage.json でどのように設定されているかを確認します。特に props オプションが true になっているかどうかで、エラーの発生パターンが変わります。

  • ルールを一時的に無効にする: 緊急の場合や、どうしてもルールに従えない特定のコードブロックがある場合は、ESLint のコメントを使って一時的にルールを無効にすることができます。しかし、これは最終手段であり、可能な限り避けるべきです。

    // eslint-disable-next-line no-param-reassign
    function myFunction(param) {
      param.someProp = 'value'; // この行だけエラーを無視
    }
    
    /* eslint-disable no-param-reassign */
    function anotherFunction(param) {
      param = 'new value';
      param.someProp = 'another value';
    }
    /* eslint-enable no-param-reassign */
    


no-param-reassign の基本設定

まず、ESLint の設定ファイル(例: .eslintrc.js または .eslintrc.json)で、このルールを有効にする方法を示します。

// .eslintrc.json
{
  "rules": {
    "no-param-reassign": "error" // 引数への再代入を禁止 (デフォルト設定)
  }
}

この設定では、引数自体に新しい値を代入する行為がエラーになります。

例1: プリミティブ型引数の再代入

悪い例 (ESLint がエラーを出す)

/* eslint no-param-reassign: "error" */

function processNumber(num) {
  // num に新しい値を再代入しているため、ESLint がエラー (no-param-reassign) を出します。
  num = num * 2; 
  console.log(num);
  return num;
}

let myValue = 10;
processNumber(myValue); // 出力: 20
console.log(myValue);   // 出力: 10 (myValue 自体は変更されない)

良い例 (ESLint がエラーを出さない)

/* eslint no-param-reassign: "error" */

function processNumber(num) {
  // 新しいローカル変数に結果を格納します。
  const doubledNum = num * 2; 
  console.log(doubledNum);
  return doubledNum;
}

let myValue = 10;
const newValue = processNumber(myValue); // 出力: 20
console.log(myValue);                   // 出力: 10
console.log(newValue);                  // 出力: 20

解説: プリミティブ型の場合、引数に再代入しても呼び出し元の変数は変更されませんが、コードの意図が不明瞭になるため禁止されます。新しい変数を宣言してそこに計算結果を格納する方が、より明確で推奨される方法です。

例2: オブジェクト型引数(オブジェクト)の再代入

悪い例 (ESLint がエラーを出す)

/* eslint no-param-reassign: "error" */

function updateSettings(settings) {
  // settings 自体に新しいオブジェクトを再代入しているため、ESLint がエラーを出します。
  settings = { ...settings, theme: 'dark' }; 
  console.log(settings);
  return settings;
}

const userSettings = { theme: 'light', notifications: true };
updateSettings(userSettings); // 出力: { theme: 'dark', notifications: true }
console.log(userSettings);    // 出力: { theme: 'light', notifications: true } (userSettings 自体は変更されない)

良い例 (ESLint がエラーを出さない)

/* eslint no-param-reassign: "error" */

function updateSettings(settings) {
  // 新しいオブジェクトを作成して返す
  const newSettings = { ...settings, theme: 'dark' }; 
  console.log(newSettings);
  return newSettings;
}

const userSettings = { theme: 'light', notifications: true };
const updatedSettings = updateSettings(userSettings); // 出力: { theme: 'dark', notifications: true }
console.log(userSettings);                           // 出力: { theme: 'light', notifications: true }
console.log(updatedSettings);                        // 出力: { theme: 'dark', notifications: true }

解説: オブジェクト型の場合でも、引数に再代入すると、その引数が別のオブジェクトを参照するようになります。これは意図せず元のオブジェクトを変更しないための一つの方法ですが、やはりルールは引数への再代入自体を禁止します。新しいオブジェクトを作成して返すのが正しい方法です。

例3: オブジェクト型引数のプロパティ変更 (props: true の場合)

この例では、ESLint の設定で props オプションを true にします。

// .eslintrc.json
{
  "rules": {
    "no-param-reassign": ["error", { "props": true }] // 引数のプロパティ変更も禁止
  }
}

悪い例 (ESLint がエラーを出す)

/* eslint no-param-reassign: ["error", { "props": true }] */

function processUser(user) {
  // user オブジェクトのプロパティを直接変更しているため、ESLint がエラーを出します。
  user.isActive = true; 
  user.loginCount++;
  console.log(user);
}

const currentUser = { name: 'Alice', isActive: false, loginCount: 5 };
processUser(currentUser); // 出力: { name: 'Alice', isActive: true, loginCount: 6 }
console.log(currentUser); // 出力: { name: 'Alice', isActive: true, loginCount: 6 } (元のオブジェクトが変更される)

良い例 (ESLint がエラーを出さない)

/* eslint no-param-reassign: ["error", { "props": true }] */

function processUser(user) {
  // 新しいオブジェクトを作成し、必要なプロパティを変更して返す
  const newUser = { 
    ...user, 
    isActive: true, 
    loginCount: user.loginCount + 1 
  };
  console.log(newUser);
  return newUser;
}

const currentUser = { name: 'Alice', isActive: false, loginCount: 5 };
const updatedUser = processUser(currentUser); // 出力: { name: 'Alice', isActive: true, loginCount: 6 }
console.log(currentUser);                    // 出力: { name: 'Alice', isActive: false, loginCount: 5 }
console.log(updatedUser);                    // 出力: { name: 'Alice', isActive: true, loginCount: 6 }

解説: props: true の場合、オブジェクトや配列の引数を直接変更する行為(ミューテーション)が禁止されます。これは、参照渡しの特性による「副作用」を防ぐための非常に重要なアプローチです。新しいオブジェクトを生成し、変更を加えた後に返すことで、元のオブジェクトの不変性を保ちます。

例4: 配列型引数の変更 (props: true の場合)

引き続き props: true の設定を使用します。

悪い例 (ESLint がエラーを出す)

/* eslint no-param-reassign: ["error", { "props": true }] */

function addIdToArray(arr, id) {
  // arr に要素を追加しているため、ESLint がエラーを出します。
  arr.push(id); 
  console.log(arr);
  return arr;
}

const ids = [101, 102];
addIdToArray(ids, 103); // 出力: [101, 102, 103]
console.log(ids);       // 出力: [101, 102, 103] (元の配列が変更される)

良い例 (ESLint がエラーを出さない)

/* eslint no-param-reassign: ["error", { "props": true }] */

function addIdToArray(arr, id) {
  // 新しい配列を作成し、要素を追加して返す
  const newArr = [...arr, id]; 
  console.log(newArr);
  return newArr;
}

const ids = [101, 102];
const updatedIds = addIdToArray(ids, 103); // 出力: [101, 102, 103]
console.log(ids);                          // 出力: [101, 102]
console.log(updatedIds);                   // 出力: [101, 102, 103]

解説: 配列もオブジェクトの一種なので、pushsplice などのメソッドで直接変更すると props: true の場合にエラーになります。スプレッド構文などを使って新しい配列を生成し、それを返すのが正しい方法です。

reduce のアキュムレータは、しばしば意図的に変更されるため、props: true の場合に例外として扱うためのオプションが用意されています。

// .eslintrc.json
{
  "rules": {
    "no-param-reassign": ["error", { 
      "props": true, 
      "ignorePropertyModificationsFor": ["acc", "accumulator"] // 'acc' や 'accumulator' という名前の引数のプロパティ変更を許可
    }]
  }
}

悪い例 (上記設定がない場合にエラー)

/* eslint no-param-reassign: ["error", { "props": true }] */
// 上記設定がない場合、`acc[item.id] = item.value;` でエラー

const items = [{ id: 'a', value: 10 }, { id: 'b', value: 20 }];
const result = items.reduce((acc, item) => {
  acc[item.id] = item.value; // 通常の reduce の使い方だが、props:true だとエラー
  return acc;
}, {});

console.log(result); // { a: 10, b: 20 }

良い例 (上記設定がある場合)

/* eslint no-param-reassign: ["error", { "props": true, "ignorePropertyModificationsFor": ["acc"] }] */

const items = [{ id: 'a', value: 10 }, { id: 'b', value: 20 }];
const result = items.reduce((acc, item) => {
  // 'acc' が ignorePropertyModificationsFor に含まれているため、エラーにならない
  acc[item.id] = item.value; 
  return acc;
}, {});

console.log(result); // { a: 10, b: 20 }

良い例 (より純粋なアプローチ)

ESLint の設定を変更したくない場合や、より関数型プログラミングのアプローチを好む場合は、reduce でも新しいオブジェクトを返すようにします。

/* eslint no-param-reassign: ["error", { "props": true }] */

const items = [{ id: 'a', value: 10 }, { id: 'b', value: 20 }];
const result = items.reduce((acc, item) => {
  // 各イテレーションで新しいオブジェクトを返す
  return { ...acc, [item.id]: item.value }; 
}, {});

console.log(result); // { a: 10, b: 20 }

解説: reduce のアキュムレータは特殊なケースです。もし、アキュムレータのプロパティ変更を許可したい場合は、ignorePropertyModificationsFor オプションを使って明示的に許可します。または、各ステップで新しいオブジェクトを生成して返すことで、ルールの制約を回避しつつ、より純粋なアプローチを維持できます。



no-param-reassign ルールは、関数の引数を直接変更する(再代入やプロパティの変更)ことを禁止し、コードの予測可能性と保守性を高めることを目的としています。このルールに違反しないようにコードを書くための、主要な代替手法は以下の通りです。

新しいローカル変数を宣言して使用する

アンチパターン(no-param-reassign 違反)

function processValue(param) {
  param = param + 10; // 引数を直接再代入
  return param;
}

代替手法(ベストプラクティス)
関数の引数を操作する代わりに、新しいローカル変数を宣言し、その変数に結果を格納します。

function processValue(param) {
  const newValue = param + 10; // 新しい変数に結果を格納
  return newValue;
}

// または、計算結果を直接返す
function processValue(param) {
  return param + 10;
}

解説: これはプリミティブ型(数値、文字列、真偽値など)の引数を扱う場合に最も一般的で簡単な解決策です。元の引数は変更されず、関数の外からの影響もありません。

新しいオブジェクトや配列を作成して返す(不変性)

これが no-param-reassign ルール、特に props: true オプションがある場合に最も重要な代替手法です。元のオブジェクトや配列を変更するのではなく、変更を加えた新しいオブジェクトや配列を作成し、それを返します。

アンチパターン(no-param-reassign 違反、特に props: true の場合)

function updateObject(obj) {
  obj.status = 'updated'; // オブジェクトのプロパティを直接変更
  return obj;
}

function addToArray(arr, item) {
  arr.push(item); // 配列を直接変更
  return arr;
}

代替手法(ベストプラクティス)
スプレッド構文 (...) や Object.assign(), Array.prototype.map(), Array.prototype.filter(), Array.prototype.concat() などの不変な操作を使用します。

オブジェクトの場合
function updateObject(obj) {
  // スプレッド構文で新しいオブジェクトを作成し、プロパティを変更
  const newObj = { ...obj, status: 'updated' }; 
  return newObj;
}

// 複数のプロパティを変更する場合も同様
function updateUser(user, newAge, newCity) {
  return {
    ...user,
    age: newAge,
    city: newCity
  };
}

// Object.assign() を使う方法(新しい空のオブジェクトにコピー)
function updateObjectWithAssign(obj) {
  return Object.assign({}, obj, { status: 'updated' });
}
配列の場合
function addToArray(arr, item) {
  // スプレッド構文で新しい配列を作成し、要素を追加
  const newArr = [...arr, item]; 
  return newArr;
}

function removeById(arr, idToRemove) {
  // filter メソッドで新しい配列を作成(元の配列は変更しない)
  return arr.filter(item => item.id !== idToRemove); 
}

function updateItemInArray(arr, idToUpdate, newPrice) {
  // map メソッドで新しい配列を作成し、特定の要素のみ変更
  return arr.map(item => item.id === idToUpdate ? { ...item, price: newPrice } : item);
}

// concat() を使う方法(単一の要素追加や配列結合)
function addToArrayWithConcat(arr, item) {
  return arr.concat(item);
}

解説: 「不変性 (Immutability)」の原則は、no-param-reassign ルールの中核をなす考え方です。データを直接変更するのではなく、変更を適用した新しいデータのコピーを作成することで、予期せぬ副作用を防ぎ、コードの追跡やデバッグを容易にします。これは特に、React の状態管理や Redux のリデューサーなど、状態管理が厳密なフレームワークやライブラリで推奨されるプラクティスです。

デフォルトパラメータや分割代入を活用する

関数のシグネチャ(引数リスト)を工夫することで、関数内で引数に再代入する状況を避けることができます。

アンチパターン(no-param-reassign 違反)

function processOptions(options) {
  // options が undefined の場合にデフォルト値を設定しようとするとエラー
  if (!options) {
    options = {}; // ここでエラー
  }
  if (!options.timeout) {
    options.timeout = 5000; // props: true ならここでエラー
  }
  return options;
}

代替手法(ベストプラクティス)
デフォルトパラメータや、引数のオブジェクトをすぐに分割代入して新しいローカル変数に展開します。

// デフォルトパラメータの活用
function processOptions(options = { timeout: 5000 }) {
  // options が undefined の場合、自動的に { timeout: 5000 } が代入される
  // これにより、関数内で options に再代入する必要がなくなる
  return options;
}

// 分割代入とデフォルト値の活用(より推奨)
function processOptions({ timeout = 5000, maxRetries = 3 } = {}) {
  // ここで timeout と maxRetries はすでにローカル変数として定義されている
  // 必要に応じて、これらの値を使って新しいオブジェクトを作成し返す
  const finalOptions = { timeout, maxRetries };
  return finalOptions;
}

// 例:
const myOptions = { maxRetries: 5 };
const processed = processOptions(myOptions); // { timeout: 5000, maxRetries: 5 }
const defaultProcessed = processOptions(); // { timeout: 5000, maxRetries: 3 }

解説: ES6 で導入されたこれらの機能は、関数の引数をクリーンに保ち、不必要な再代入を避けるのに非常に役立ちます。関数の冒頭で引数を安全に操作するための強力なツールです。

ルールのオプションを利用する(限定的)

特定のケース(例: Array.prototype.reduce のアキュムレータ)で、引数のプロパティ変更が意図的な設計パターンである場合、ESLint の ignorePropertyModificationsFor オプションを使用して、その引数に対するチェックを無効にできます。

使用例:

// .eslintrc.json
{
  "rules": {
    "no-param-reassign": ["error", { 
      "props": true,
      "ignorePropertyModificationsFor": ["acc", "memo"] // 'acc' や 'memo' という名前の引数のプロパティ変更を許可
    }]
  }
}

解説: これは厳密には代替手法ではありませんが、特定の例外を扱うための重要な設定です。これにより、一般的な不変性の原則を維持しつつ、特定のパターンを許容することができます。ただし、乱用は避けるべきです。

no-param-reassign ルールに準拠することは、JavaScript においてより堅牢で保守しやすいコードを書くための良い習慣です。

  • 例外: reduce のアキュムレータのような特定のパターンでは、ESLint のオプションを使ってルールを調整する。
  • 関数の引数処理: デフォルトパラメータや分割代入を積極的に活用し、関数内部で引数を再構築する手間を省く。
  • オブジェクト/配列型: 元のオブジェクトや配列を変更せず、スプレッド構文や他の不変なメソッドを使って新しいオブジェクト/配列を作成し、それを返す。
  • プリミティブ型: 新しいローカル変数を宣言し、計算結果をそこに格納するか、直接返す。