0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript】filterでnull除外してから分割代入を使う安全パターン ~forEachの引数ミスで学んだこと~

Posted at

TL;DR(この記事で解決できること)

  • filterでnull/undefinedを事前除外して、安全に分割代入を使う方法
  • forEachの引数での分割代入の正しい使い方(よくある間違いと対策)
  • 日別データ集計でのnull安全なパターン実装

こんな人に読んでほしい

  • JavaScriptでnull/undefinedエラーに悩んでいる人
  • forEachやmapで分割代入を使いたいが、エラーが心配な人
  • 配列操作のベストプラクティスを探している人

環境

項目 バージョン/詳細
Node.js v20.x 以上
対応ブラウザ Chrome 120+, Firefox 120+, Safari 17+
ECMAScript ES2015以上(分割代入、デフォルト引数使用)

1. 問題:forEachで分割代入したらエラーになった

SmallWinsEngine(トレーニング記録の集計エンジン)の実装中、以下のようなエラーに遭遇しました。

エラーメッセージ

TypeError: Cannot destructure property 'duration' of 'undefined' as it is undefined
    at Array.forEach (<anonymous>)
    at SmallWinsEngine.calculateMetrics (SmallWinsEngine.js:45:23)

問題のコード

// ❌ エラーになる可能性があるコード
const cardioWorkouts = workouts.filter((workout) =>
  workout.exerciseType === 'cardio'
);

cardioWorkouts.forEach(({ duration, date }) => {
  // workoutsにnullやundefinedが含まれていると...
  // TypeError: Cannot destructure property 'duration' of 'undefined'
  totalSeconds += Number(duration) || 0;
});

実装中は「filterで絞っているから大丈夫だろう」と思っていましたが、配列内にnullやundefinedが混入している場合、workout.exerciseTypeの評価でエラーになることがありました。

2. 原因:配列内のnull/undefinedを考慮していなかった

根本原因の分析

この問題には3つの原因がありました:

  1. 配列にnullやundefinedが混入する可能性を見落としていた

    • APIレスポンスや外部データソースでは想定外の値が含まれることがある
  2. forEachの引数で直接分割代入すると、防御的コードが書けない

    • 分割代入は便利だが、nullチェックを同時に行えない
  3. データ検証とデータ処理を同じ場所で行っていた

    • 責任が混在し、コードの意図が不明確に

よくある誤解:forEachの引数パターン

私が最初に試みた、完全に間違ったパターンも共有します:

// ❌ これは完全に間違い!
strengthWorkouts.forEach((date, sets = 0, reps = 0) => {
  // 第1引数は要素、第2引数はindex、第3引数は配列全体
  // setsとrepsはindexとarrayになってしまう
});

// forEachの正しい引数構造
array.forEach((element, index, array) => {
  // element: 現在の要素
  // index: 現在のインデックス
  // array: 元の配列全体
});

3. 解決策:filterで事前検証 → 安全な分割代入

パターン1: filter + forEach(推奨)

// ✅ null/undefined を事前に除外
const validCardioWorkouts = workouts.filter(workout =>
  workout && workout.exerciseType === 'cardio'  // null安全
);

// filterで安全性が保証されたので分割代入可能
validCardioWorkouts.forEach(({ duration = 0, date }) => {
  totalSeconds += Number(duration) || 0;
  trainingDays.add(date);
});

パターン2: 分割代入の判断基準を明文化

コードレビューの観点から、分割代入をいつ使うべきかの判断基準を作りました:

// 分割代入パターンの選択基準
const destructuringDecisionTree = {
  // パターンA: 引数での分割代入
  useInArgument: {
    when: [
      'プロパティ数 <= 3',
      'filterでnull除外済み',
      'デフォルト値で対応可能'
    ],
    example: '({ id, name = "", age = 0 }) => { ... }'
  },

  // パターンB: 関数内での分割代入
  useInBody: {
    when: [
      'プロパティ数 > 3',
      'nullチェックが必要',
      'デバッグのため元オブジェクトも必要'
    ],
    example: '(item) => { if (!item) return; const { ... } = item; }'
  }
};

4. 実装例:SmallWinsEngineでの日別集計

実際のプロジェクトで使用しているコードを紹介します:

筋トレデータの集計実装

class SmallWinsEngine {
  calculateMetrics(workouts) {
    // Step1: 筋トレデータのフィルタリング(null安全)
    const strengthWorkouts = workouts.filter(workout =>
      workout && workout.exerciseType === 'strength'
    );

    const byDay = {};
    let totalSets = 0;
    let totalReps = 0;
    const trainingDays = new Set();

    // Step2: filterで安全性確保済みなので、分割代入が使える
    strengthWorkouts.forEach(({ date, sets = 0, reps = 0 }) => {
      // 日別集計の実装
      if (!byDay[date]) {
        byDay[date] = { sets: 0, reps: 0 };
      }
      byDay[date].sets += Number(sets) || 0;
      byDay[date].reps += Number(reps) || 0;

      // 総計の更新
      totalSets += Number(sets) || 0;
      totalReps += Number(reps) || 0;

      // ユニークな日数カウント
      trainingDays.add(date);
    });

    return {
      byDay,
      totalSets,
      totalReps,
      trainingDays: trainingDays.size
    };
  }
}

まとめ:学んだベストプラクティス

重要なポイント

  1. データパイプライン設計: filter(検証)→ forEach(処理)で責任を分離
  2. 分割代入の判断基準: 3プロパティ以下&null安全なら引数で、それ以外は関数内で
  3. 累積パターンの理解: ||演算子は初期値提供、累積は+=で実現

ESLint設定の推奨

エラーを未然に防ぐため、以下のESLintルールを設定することをお勧めします:

{
  "rules": {
    "no-unsafe-optional-chaining": "error",
    "default-param-last": "warn",
    "no-param-reassign": ["error", {
      "props": true,
      "ignorePropertyModificationsFor": ["accumulator"]
    }]
  }
}

日別集計パターンのまとめ

最後に、今回学んだ日別集計パターンをまとめます:

// キーポイント:用途によってbyDayの構造を変える

// 単一値の集計(例: 有酸素運動の分数)
const byDaySimple = {};
cardioWorkouts.forEach(({ date, duration = 0 }) => {
  byDaySimple[date] = (byDaySimple[date] || 0) + duration;
});

// 複数値の集計(例: 筋トレのセット数とレップ数)
const byDayComplex = {};
strengthWorkouts.forEach(({ date, sets = 0, reps = 0 }) => {
  if (!byDayComplex[date]) {
    byDayComplex[date] = { sets: 0, reps: 0 };
  }
  byDayComplex[date].sets += sets;
  byDayComplex[date].reps += reps;
});

参考リンク


最後まで読んでいただきありがとうございました!
この記事が同じ問題で困っている方の参考になれば幸いです。

もし記事が役に立ったら、LGTMボタンをお願いします👍
コメントやフィードバックもお待ちしています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?