deepmerge の使い方 — JavaScriptオブジェクトの深いマージを簡単に実現
一言でいうと
deepmerge は、JavaScriptオブジェクトを再帰的(ディープ)にマージするライブラリです。ネストされたオブジェクトのプロパティも正しく結合でき、元のオブジェクトを変更しないイミュータブルな操作を提供します。
どんな時に使う?
- 設定オブジェクトのマージ — デフォルト設定とユーザー設定を深い階層まで正しく結合したいとき
- 状態管理でのパーシャルアップデート — Redux や Zustand などで、ネストされた状態オブジェクトの一部だけを更新したいとき
- 複数ソースからのデータ統合 — API レスポンスや複数の設定ファイルなど、複数のオブジェクトを1つにまとめたいとき
インストール
# npm
npm install deepmerge
# yarn
yarn add deepmerge
# pnpm
pnpm add deepmerge
UMD バンドル(minified+gzip で 723B)も提供されており、ブラウザで直接利用することも可能です。
基本的な使い方
import merge from 'deepmerge';
const defaultConfig = {
api: {
baseUrl: 'https://api.example.com',
timeout: 3000,
headers: {
'Content-Type': 'application/json',
},
},
retry: {
count: 3,
delay: 1000,
},
};
const userConfig = {
api: {
timeout: 5000,
headers: {
Authorization: 'Bearer xxx',
},
},
retry: {
count: 5,
},
};
const finalConfig = merge(defaultConfig, userConfig);
console.log(finalConfig);
// {
// api: {
// baseUrl: 'https://api.example.com',
// timeout: 5000,
// headers: {
// 'Content-Type': 'application/json',
// Authorization: 'Bearer xxx',
// },
// },
// retry: {
// count: 5,
// delay: 1000,
// },
// }
Object.assign やスプレッド構文ではネストされた headers が丸ごと上書きされてしまいますが、deepmerge なら深い階層まで正しくマージされます。
よく使う API — deepmerge の使い方詳細
1. merge(x, y, [options]) — 2つのオブジェクトをマージ
最も基本的な使い方です。第2引数(y)の値が優先されます。
import merge from 'deepmerge';
const x = { foo: { bar: 3 } };
const y = { foo: { baz: 4 }, quux: 5 };
const result = merge(x, y);
// { foo: { bar: 3, baz: 4 }, quux: 5 }
元の x と y は変更されません(新しいオブジェクトが返されます)。
2. merge.all(arrayOfObjects, [options]) — 複数オブジェクトを一括マージ
3つ以上のオブジェクトをまとめてマージしたい場合に使います。
import merge from 'deepmerge';
const base = { theme: { color: 'blue', fontSize: 14 } };
const override1 = { theme: { color: 'red' }, debug: false };
const override2 = { theme: { fontWeight: 'bold' }, debug: true };
const result = merge.all([base, override1, override2]);
// {
// theme: { color: 'red', fontSize: 14, fontWeight: 'bold' },
// debug: true,
// }
3. arrayMerge オプション — 配列のマージ戦略をカスタマイズ
デフォルトでは配列は**連結(concat)**されます。上書きしたい場合はカスタム関数を渡します。
import merge from 'deepmerge';
// 配列を上書きする戦略
const overwriteMerge = (
_destinationArray: unknown[],
sourceArray: unknown[],
_options: merge.Options
): unknown[] => sourceArray;
const result = merge(
{ tags: ['frontend', 'react'] },
{ tags: ['backend', 'node'] },
{ arrayMerge: overwriteMerge }
);
// { tags: ['backend', 'node'] }
同じインデックスのオブジェクトを結合する戦略も実装できます。
import merge from 'deepmerge';
const combineMerge: merge.Options['arrayMerge'] = (target, source, options) => {
const destination = target.slice();
source.forEach((item, index) => {
if (typeof destination[index] === 'undefined') {
destination[index] = options.cloneUnlessOtherwiseSpecified(item, options);
} else if (options.isMergeableObject(item)) {
destination[index] = merge(target[index], item, options);
} else if (target.indexOf(item) === -1) {
destination.push(item);
}
});
return destination;
};
const result = merge(
[{ a: true }],
[{ b: true }, 'extra'],
{ arrayMerge: combineMerge }
);
// [{ a: true, b: true }, 'extra']
4. customMerge オプション — プロパティごとにマージ関数を指定
特定のキーだけ独自のマージロジックを適用したい場合に使います。
import merge from 'deepmerge';
interface User {
name: { first: string; last: string };
pets: string[];
}
const alex: User = {
name: { first: 'Alex', last: 'Alexson' },
pets: ['Cat', 'Parrot'],
};
const tony: User = {
name: { first: 'Tony', last: 'Tonison' },
pets: ['Dog'],
};
const result = merge(alex, tony, {
customMerge: (key) => {
if (key === 'name') {
return (a: User['name'], b: User['name']) =>
({ first: `${a.first} & ${b.first}`, last: b.last });
}
// undefined を返すとデフォルトのマージが適用される
},
});
console.log(result.name); // { first: 'Alex & Tony', last: 'Tonison' }
console.log(result.pets); // ['Cat', 'Parrot', 'Dog']
5. isMergeableObject オプション — マージ対象の判定をカスタマイズ
デフォルトではほぼすべてのオブジェクトのプロパティが再帰的にマージされます。クラスインスタンスなどをそのままコピーしたい場合は、is-plain-object と組み合わせます。
import merge from 'deepmerge';
import { isPlainObject } from 'is-plain-object';
class Config {
constructor(public value: string) {}
}
const target = { setting: { name: 'default' } };
const source = { setting: new Config('custom') };
// デフォルト: Config のプロパティが展開されてしまう
const defaultResult = merge(target, source);
console.log(defaultResult.setting instanceof Config); // false
// isPlainObject を使う: Config インスタンスがそのまま保持される
const customResult = merge(target, source, {
isMergeableObject: isPlainObject,
});
console.log(customResult.setting instanceof Config); // true
類似パッケージとの比較
| パッケージ | サイズ (min+gz) | 配列マージ | カスタマイズ性 | TypeScript 型定義 | 特徴 |
|---|---|---|---|---|---|
| deepmerge | ~723B | concat(カスタム可) | 高い | 同梱 | 軽量・実績豊富 |
| lodash.merge | ~5KB | インデックスベース | 低い | @types/lodash | lodash エコシステムの一部 |
| deepmerge-ts | ~1KB | concat(カスタム可) | 高い | TypeScript ネイティブ | TypeScript ファーストで型推論が強力 |
| spread 構文 | 0B | 不可(上書き) | なし | — | 1階層のみ。ネストには非対応 |
補足: TypeScript で厳密な型推論(マージ後の型が自動的に推論される)を求める場合は deepmerge-ts も検討に値します。
注意点・Tips
配列のデフォルト挙動に注意
デフォルトでは配列は連結されます。「上書きしてほしい」と期待してハマるケースが非常に多いです。意図に合わない場合は arrayMerge オプションを必ず指定しましょう。
// 意図しない結果になりがちな例
merge({ ids: [1, 2] }, { ids: [3] });
// => { ids: [1, 2, 3] } ← 上書きではなく連結される
ESM インポートについて
deepmerge v4.x では Webpack のバグに起因して ESM エントリポイントが削除されています。import merge from 'deepmerge' は多くのバンドラーで動作しますが、内部的には CommonJS が読み込まれます。
clone オプションは非推奨
clone: false を渡すと子オブジェクトがクローンされずに直接参照されます。v2.x 以前の互換性のために残されていますが、意図しない副作用の原因になるため、特別な理由がない限り使わないでください。
循環参照には対応していない
循環参照を含むオブジェクトを渡すとスタックオーバーフローが発生します。循環参照がある場合は事前に検出するか、対応した別のライブラリを使用してください。
型定義の活用
deepmerge は TypeScript の型定義を同梱しています。ジェネリクスを使って戻り値の型を明示できます。
import merge from 'deepmerge';
interface AppConfig {
api: { baseUrl: string; timeout: number };
debug: boolean;
}
const config = merge<AppConfig>(
{ api: { baseUrl: '', timeout: 3000 }, debug: false },
{ api: { baseUrl: 'https://api.example.com', timeout: 5000 }, debug: true }
);
// config は AppConfig 型として推論される
まとめ
deepmerge は、ネストされたオブジェクトの再帰的マージという頻出課題を、わずか 723B で解決する軽量ライブラリです。arrayMerge・customMerge・isMergeableObject の3つのオプションを理解しておけば、ほとんどのユースケースに対応できます。設定のマージや状態の部分更新など、スプレッド構文では手が届かない場面で積極的に活用しましょう。