# deepmerge: recursive object merging for JavaScript, and where it stops being the right tool

> TehShrike/deepmerge merges the enumerable properties of two or more objects recursively, returns a new object, and lets you override how arrays and special types are handled. It is small, MIT-licensed and deliberately narrow.

**TehShrike/deepmerge** — A library for deep (recursive) merging of Javascript objects

- Repository: https://github.com/TehShrike/deepmerge
- Stars: 2,816 · Forks: 218
- Language: JavaScript
- License: MIT
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/tehshrike-deepmerge

## What deepmerge actually solves, and who it is for

Shallow spread or Object.assign replaces a nested object wholesale. If x has foo: { bar: 3 } and y has foo: { baz: 4 }, a shallow merge leaves you with only foo.baz. deepmerge walks into both objects and produces foo: { bar: 3, baz: 4 }. The README states the goal plainly: "Merges the enumerable properties of two or more objects deeply."

The audience is narrow and mostly server-side or build-tool adjacent. Configuration layering is the classic case: a base config object, an environment override, and a per-tenant override, combined into one effective config. The same shape appears in test fixture builders, in Redux-style state updates where a reducer receives a partial patch, and in tools that merge package manifests or JSON schemas. It is a utility, not a framework, and it does not try to be anything else. If your data is flat, you do not need it; Object.assign is faster to read and has no options surface to learn.

## How the recursive merge works, and why arrays are the surprise

merge(x, y, [options]) returns a new merged object, and the README is explicit that "neither x or y is modified." When the same key exists on both sides, the value from y wins. When both values are objects that the library considers mergeable, it recurses instead of overwriting.

Arrays are the part that catches people. The README notes: "By default, arrays are merged by concatenating them." So merging [1, 2, 3] with [3, 2, 1] yields a six-element array, not [3, 2, 1]. That default is a design decision, not an accident, and it is the single most common source of wrong output. The README also documents that the current concatenation behaviour replaced an older algorithm: combining objects at the same index "was the default array merging algorithm pre-version-2.0.0." Anyone upgrading across that boundary should expect different results from the same inputs.

The second mechanism is isMergeableObject. By default deepmerge "clones every property from almost every kind of object," which means an instance of a custom class loses its prototype in the result. The README shows this directly: after a default merge, the merged property is no longer an instance of the original constructor. Passing a stricter predicate, such as isPlainObject from is-plain-object, changes the outcome so the instance survives intact but its sibling properties are not merged. You are choosing between structural merging and identity preservation, and you cannot have both from one call.

## Installing deepmerge and running a first merge

The README gives npm as the install path. The package declares a CommonJS entry point through main: dist/cjs.js, and the README says the ESM entry point "was dropped due to a Webpack bug," linking webpack issue 6584. That is a real constraint for anyone on a pure ESM toolchain.

```bash
npm install deepmerge
```

After installing, require it and merge two nested objects. The README's own example uses this shape:

```js
const merge = require('deepmerge')

const x = { foo: { bar: 3 }, array: [{ does: 'work', too: [1, 2, 3] }] }
const y = { foo: { baz: 4 }, quux: 5, array: [{ does: 'work', too: [4, 5, 6] }, { really: 'yes' }] }

const output = merge(x, y)
// foo: { bar: 3, baz: 4 }, quux: 5, and a three-element array
```

What you should see is a new object with foo.bar and foo.baz both present, quux copied from y, and the array grown rather than replaced. If you expected the array to be replaced, that is the concatenation default at work.

To merge more than two objects, the README provides merge.all, which takes an array of objects and folds them into one result. It accepts the same options object.

```js
const foobar = { foo: { bar: 3 } }
const foobaz = { foo: { baz: 4 } }
const bar = { bar: 'yay!' }

merge.all([foobar, foobaz, bar]) // => { foo: { bar: 3, baz: 4 }, bar: 'yay!' }
```

If you want arrays overwritten instead of concatenated, the README's overwriteMerge example is a one-liner passed through the arrayMerge option. The repository also exposes a UMD build, so the README notes it can be loaded from unpkg without a bundler. The README does not document a rollback procedure for a bad merge result, because the function is pure and the inputs are untouched.

## customMerge, and the limits of per-key overrides

customMerge is the escape hatch for keys that should not be merged structurally. It receives the property key and returns the function that should handle that key, or undefined to fall back to default behaviour. The README's example merges two name objects into the string "Alex and Tony" while leaving the pets array to the default concatenation.

This is the most useful option in the library and also the most easily misused. The returned function has no access to the key it was called for beyond your own closure, and there is no built-in way to express "merge this key with these options but that key with those options" through a single options object. If your configuration needs three different array strategies in three different places, you will be writing the dispatch logic yourself, and the library will not validate it. There is no documented error for a customMerge that returns a non-function; the README simply does not cover that case.

The clone option is marked deprecated in the README, which says it defaults to true and that setting it to false copies child objects directly, matching pre-2.x behaviour. A deprecated option that still changes output is a maintenance signal worth reading literally: the README does not say when it will be removed.

## Where deepmerge is the wrong tool

The clearest failure mode is merging class instances and expecting the prototype to survive. The README's own example shows instanceof SuperSpecial returning false after a default merge. If your objects carry methods or internal state that lives on the prototype, deepmerge will flatten them into plain properties, and calling a method on the result will fail at runtime. The workaround is isMergeableObject: isPlainObject, but that trades prototype preservation for losing the merge on that key. Neither option is a fix for data that should not be structurally merged at all.

The second wrong-tool case is ESM. The README states the ESM entry point was dropped because of a Webpack bug. Modern bundlers handle CommonJS interop, but if your project requires a native ESM export map or runs in an environment that refuses CommonJS, this library is not the drop-in you want, and the README offers no ESM path.

The third case is large or cyclic structures. The README does not document cycle detection, and it does not document a depth limit. Nothing in the repository layout or the README suggests those guards exist. If your inputs can contain circular references, treat that as unverified and test it yourself before shipping. Finally, if you only ever merge flat objects, the recursive walk and the options surface are overhead you are paying for nothing.

## deepmerge vs lodash merge and the arrayMerge difference

The comparison people reach for is lodash's merge. Both recurse into nested objects and both return a new object rather than mutating inputs, but the array behaviour differs in a way that changes results. deepmerge concatenates arrays by default, as its README states. lodash's merge assigns array values by index, replacing elements positionally. For a config override where a user supplies a shorter array, deepmerge grows the list and lodash replaces the head of it. Neither is wrong; they encode different assumptions about what an array means.

The second difference is configurability. deepmerge exposes arrayMerge, isMergeableObject and customMerge as documented options with worked examples. That is more surface area than a one-function merge, and it exists because the library refuses to guess. The cost is that the default is not the behaviour most people expect the first time they merge two arrays, and the README has to spend a section explaining it.

A third difference is scope. deepmerge is a single-purpose package with a UMD bundle the README describes as 723B minified and gzipped, and a dependency-free runtime. If you already ship lodash, adding deepmerge duplicates a capability you have. If you do not, deepmerge is the smaller commitment.

## Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-09-24. The package.json reports version 4.3.1 and engines: node >=0.10.0, which is an unusually low floor and suggests the maintainers are not chasing modern runtime features. The changelog.md file is present at the top level, so version history is tracked in-repo rather than only in release notes.

The licence is MIT, declared in package.json and in license.txt. That permits commercial use and modification, and it requires the copyright notice and permission notice to be retained. This is a factual description of the licence text, not legal advice; if your organisation has a policy on third-party licences, route it through that process.

Upgrade cost is concentrated in one place: the 2.0.0 array merge change. The README documents that the index-combining algorithm was the default before 2.0.0, which means code written against 1.x and never revisited may produce different arrays after an upgrade without any error being raised. There is no deprecation warning for that behaviour change because it already happened. The deprecated clone option is the remaining loose end the README flags. Test coverage is exercised through npm test, which the README lists as running tape over test/*.js plus jsmd over readme.md and a TypeScript check, so the README examples are themselves part of the test run.

## Conclusion

Adopt deepmerge when you need a small, predictable recursive merge of plain JavaScript objects and you are willing to decide explicitly how arrays should behave. Do not adopt it if you need ESM as the entry point, if you expect arrays to be overwritten by default, or if you need to merge class instances and keep their prototypes without configuring isMergeableObject. Before committing, verify three things in your own code: the arrayMerge strategy you want, whether isPlainObject from is-plain-object is the right isMergeableObject for your data, and whether the CommonJS-only entry point fits your bundler.

## FAQ

### What is deep merge in the context of the deepmerge library?

It means recursively merging nested objects rather than replacing a nested value wholesale. deepmerge walks into objects that both sides share and combines their properties, returning a new object so neither input is modified.

### How does deepmerge differ from a shallow merge?

A shallow merge on { foo: { bar: 3 } } and { foo: { baz: 4 } } leaves only foo.baz, because the second foo replaces the first. deepmerge recurses and produces foo with both bar and baz, and it also concatenates arrays by default rather than replacing them.

### How do I merge an object with deepmerge?

Install it with npm install deepmerge, require it, and call merge(x, y). The result is a new object where values from y win on conflicting keys, and merge.all takes an array of objects to fold several into one.

### Is there a deepmerge alternative?

lodash's merge is the common alternative. Both recurse into nested objects, but lodash assigns array values by index while deepmerge concatenates arrays by default, and deepmerge exposes arrayMerge, isMergeableObject and customMerge as documented options.

## Sources

- [Issues](https://github.com/TehShrike/deepmerge/issues)
- [License: MIT](https://github.com/TehShrike/deepmerge/blob/master/LICENSE)
- [README](https://github.com/TehShrike/deepmerge/blob/master/README.md)
- [TehShrike/deepmerge on GitHub](https://github.com/TehShrike/deepmerge)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tehshrike-deepmerge
