🕌

浅いマージと深いマージ(Javascript)

2025/02/15に公開

オブジェクト(や配列)を合成する「マージ(merge)」には、大きく分けて**「浅いマージ」「深いマージ」**があります。ここでは、Javascriptで例をご紹介します。


浅いマージ (shallow merge) 🏝

概要

  • 一番外側(トップレベル)のプロパティだけを合成します。
  • ネストされたオブジェクトや配列は、まるごと「参照ごと」置き換え られるので、元のデータが失われることがある。

こんなときに使う

  1. ネストが浅く、配列やオブジェクトを完全に置き換えても問題ないとき
  2. そもそも変更箇所が限定的で、細かいフィールドを保持する必要がないとき

浅いマージの例

データ構造

type Data = {
  userId: number;
  userName: string;
  address: {
    city: string;
    zip: string;
    tags: string[];  // 配列を追加
  };
};

const originalData: Data = {
  userId: 123,
  userName: '空条承太郎',
  address: {
    city: 'Tokyo',
    zip: '100-0001',
    tags: ['tag1', 'tag2'], // 複数のタグを持っているとする
  },
};

const updatedData: Partial<Data> = {
  userName: '空条承太郎 (Stand User)', // userNameを変更
  address: {
    city: 'Kyoto',
    tags: ['updatedTag'], // 配列を更新
  },
};

浅いマージの実行

// 浅いマージ (shallow merge)
const shallowMerged = {
  ...originalData,
  ...updatedData,
};

console.log(shallowMerged);
/*
  結果:
  {
    userId: 123,
    userName: '空条承太郎 (Stand User)',
    address: {
      city: 'Kyoto',
      tags: ['updatedTag'],
      // zip はどこに行った?
      // → address がまるごと上書きされ、zip が失われる
    }
  }
*/

⚠️ 注意
このように「元の address にあった zip フィールド」は、updatedData.address に含まれていないため消えてしまいます
また、配列 tags も元の ['tag1', 'tag2'] を保持せず、['updatedTag'] に置き換わります。


深いマージ (deep merge) 🔎

概要

  • ネストされたオブジェクトや配列を部分的に上書きし、残したいものは保持するアプローチです。
  • 更新したいプロパティだけを上書きし、他は元のデータをそのままにしておくことができます。

こんなときに使う

  1. ネストの深い構造の一部だけ変更し、他を消さずに保持したいとき
  2. 既存の情報を維持しつつ、新しい情報だけを追加・修正したいとき

部分的に更新するサンプルコード

const deepMerged = {
  ...originalData,
  ...updatedData, // まず、一番外側(トップレベル)のプロパティを上書き
  address: {
    ...originalData.address, // 元の address は維持
    ...updatedData.address,  // 新しい address の情報で必要な部分を上書き
  },
};

console.log(deepMerged);
/*
  結果:
  {
    userId: 123,
    userName: '空条承太郎 (Stand User)',   // userName を更新
    address: {
      city: 'Kyoto',                      // city は上書き
      zip: '100-0001',                    // 元の zip を保持
      tags: ['updatedTag'],               // 配列は置き換え → ['updatedTag']
    }
  }
*/

✅ これなら zip が残っています。ただし、配列については単純にスプレッド演算子で展開すると、元の配列と合体 するのではなく 置き換え になる点にも注意が必要です。もし配列を結合したい場合は、... を使って手動でマージする(例:tags: [...originalData.address.tags, ...updatedData.address.tags] のように)などの工夫が必要です。


再帰的に深いマージを行う関数の例 🏗

もし、もっと深いネストがあって毎回スプレッド演算子を追加するのが大変…という場合には、「再帰的に深いマージを行う関数」を利用する方法が便利です。

function deepMerge<T extends object, U extends object>(target: T, source: U): T & U {
  for (const key of Object.keys(source)) {
    const srcVal = source[key];
    const tgtVal = target[key];

    // ネストされたオブジェクトなら再帰的にマージ
    if (isObject(srcVal) && isObject(tgtVal)) {
      deepMerge(tgtVal, srcVal);
    } else {
      // それ以外は単純に上書き
      target[key] = srcVal;
    }
  }
  return target as T & U;
}

function isObject(obj: unknown): obj is Record<string, any> {
  return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
}

// 使い方:
const mergedResult = deepMerge({ ...originalData }, updatedData);
console.log(mergedResult);

既存のライブラリを使う方法

自前の実装が面倒なら、以下のようなライブラリで手軽に再帰的マージを実現できます。

npm install deepmerge
import merge from 'deepmerge';

const mergedByLibrary = merge(originalData, updatedData);
console.log(mergedByLibrary);

テストのポイント 🧪

  1. 未更新フィールドの保持
    • 新しいデータに含まれないプロパティは、元の値がそのまま残っているか確認。
  2. 上書き対象のプロパティ
    • 変更したい部分だけが正しく変わっているかチェック。
  3. 配列のマージ挙動
    • スプレッド演算子などを使うと単純に置き換えになるので、場合によっては結合を工夫する必要あり。
    • ライブラリの挙動も事前に要確認。
  4. ネストが深いケース
    • 再帰的なマージをちゃんとテストし、さらに入れ子になったプロパティが消えていないか、想定外に上書きされていないかなどをチェック。

まとめ 💡

  • 浅いマージ (shallow merge)

    • 外側のプロパティだけを合成し、ネストされたオブジェクトや配列は置き換えられる。
    • 要件によっては、意図せず元のデータが消えてしまうリスクあり。
  • 深いマージ (deep merge)

    • ネストされた構造の内部まで上書きし、保持したい部分はそのままにできる。
    • 更新箇所が多い場合はライブラリや再帰処理の活用が便利。

どちらを選ぶ?

  • 全体を置き換えても平気 → 浅いマージがシンプル
  • 一部だけ変更し、他は残したい → 深いマージで安全に更新

補足:🔎「再帰的」とは?

「再帰的 (さいきてき)」とは、同じ処理を繰り返すことで、階層構造の奥深くまでアプローチすること を指します。
プログラムでいう「再帰 (recursion)」は、関数の中で自分自身を呼び出す手法で、ネストしたデータに対しても同じ処理を行いながら最深部まで到達できます。

補足:⚠️ structuredClone との違い

最後に、JavaScript のビルトイン関数として登場した structuredClone について説明します。

structuredClone とは?

  • オブジェクトや配列などの値を 「深く」コピー する関数。
  • つまり「元のオブジェクトの全階層」を丸ごと新しいメモリ領域にクローンします。
  • しかし、単に「複製」するだけであって、複数のオブジェクトを合成(マージ)する機能はありません。

用途の違い

  • structuredClone:
    • オブジェクトを一つまるごと深くコピーする
    • オリジナルのオブジェクトを保護しつつ、安全に同じ構造を別の変数へ作りたい場合に有効。
    • 例:const copy = structuredClone(originalData)
  • 深いマージ (deep merge):
    • 2つ以上のオブジェクトを再帰的に統合して、一部を上書き・未更新の部分は維持する
    • structuredClone はあくまでコピー専用なので、この動作は行いません。
// 元となるオブジェクト
const originalData = {
  userId: 123,
  userName: '空条承太郎',
  address: {
    city: 'Tokyo',
    zip: '100-0001',
    tags: ['tag1', 'tag2'],
  },
};

// structuredClone に渡す
const copiedData = structuredClone(originalData);

console.log(copiedData);
/*
  {
    userId: 123,
    userName: '空条承太郎',
    address: {
      city: 'Tokyo',
      zip: '100-0001',
      tags: ['tag1', 'tag2']
    }
  }
*/

Discussion