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つの原因がありました:
-
配列にnullやundefinedが混入する可能性を見落としていた
- APIレスポンスや外部データソースでは想定外の値が含まれることがある
-
forEachの引数で直接分割代入すると、防御的コードが書けない
- 分割代入は便利だが、nullチェックを同時に行えない
-
データ検証とデータ処理を同じ場所で行っていた
- 責任が混在し、コードの意図が不明確に
よくある誤解: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
};
}
}
まとめ:学んだベストプラクティス
重要なポイント
-
データパイプライン設計:
filter
(検証)→forEach
(処理)で責任を分離 - 分割代入の判断基準: 3プロパティ以下&null安全なら引数で、それ以外は関数内で
-
累積パターンの理解:
||
演算子は初期値提供、累積は+=
で実現
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;
});
参考リンク
- MDN - Array.prototype.filter() - filterメソッドの仕様
- MDN - Destructuring assignment - 分割代入の詳細
最後まで読んでいただきありがとうございました!
この記事が同じ問題で困っている方の参考になれば幸いです。
もし記事が役に立ったら、LGTMボタンをお願いします👍
コメントやフィードバックもお待ちしています。