Use spread syntax: const merged = { ...a, ...b }. It builds a new object with the properties of both, and when a key is in both, the value from the later object (b) wins. Object.assign(a, b) does the same merge but writes into a.
Merging two objects comes down to three questions. Which value wins when both have the same key (the later one, with every built-in method)? Do you want a new object or to change an existing one in place? And what happens to nested objects: the built-ins replace them wholesale, so a real deep merge needs a few lines of your own. The last section covers the other half of the question people ask: adding a property only when some condition holds. Each example runs on this page: hit Run, then edit the code and run it again.
1Spread syntax: { ...a, ...b }Recommended
Object spread (ES2018) copies the own enumerable properties of each object into a new object literal, left to right. A later property with the same key overwrites the earlier one, so the object you want to take priority goes last. The inputs are never modified, and you can spread as many objects as you like and mix in literal keys anywhere in the list.
Output
Prints { host: 'localhost', port: 9090, debug: true }, then the unchanged defaults. Swapping the order prints { port: 8080, debug: false, host: 'localhost' }: the values now come from defaults, and the key order follows where each key first appeared, not which object won. The last line prints { host: 'db.internal', port: 9090, debug: true, timeout: 30 }. This is the same idea as Python's {**a, **b}.
2Object.assign (merges into its first argument)
Object.assign(target, ...sources) (ES2015) was the standard answer before spread existed, and it follows the same last-wins rule. The difference is where the result goes: it copies every source into the target and returns that same target object. Passing your defaults as the first argument therefore rewrites them for every other piece of code that holds a reference. Pass a fresh {} as the target when you want a copy.
Output
The first call prints { host: 'localhost', port: 9090, debug: true } for defaults itself, and true: the return value is the target. With {} as the target you get the same merged object while base stays { host: 'localhost', port: 8080 }. Today Object.assign({}, a, b) and { ...a, ...b } are interchangeable for plain data; reach for Object.assign when you really do want to update an existing object, such as this in a constructor. One subtle difference: Object.assign sets properties, so it runs setters on the target, while spread only defines plain properties on a new object.
3Shallow merge: nested objects are replaced, not merged
Both spread and Object.assign are shallow. They only look at the top-level keys, so when both objects have a nested object under the same key, the later one replaces the earlier one entirely. They also don't copy nested objects: the merged object points at the very same inner objects as its inputs, so changing one through the copy changes the original.
Output
Prints { db: { port: 6543 }, debug: true }: db.host is gone, because override.db replaced base.db as a whole. Then 1 and true: editing copy.db edited base.db, since they are the same object. For configuration objects with nested sections, that first result is usually a bug, which is what the next section fixes.
4A small recursive deep merge
JavaScript has no built-in deep merge. The usual fix is a short recursive function: walk the source's keys, and when both sides hold a plain object, merge those two recursively; otherwise the source value wins. Deciding what counts as a "plain object" is the important part. Arrays, Dates, Maps and class instances all have typeof "object", but you almost never want to merge their insides, so the check below only accepts objects whose prototype is Object.prototype (or null).
Output
merged.db prints { host: 'localhost', port: 6543, pool: { min: 1, max: 20 } }: host and pool.min survive from base two levels down. The array and the date are treated as single values, so the next line prints [ 'c' ] 2026-09-01T00:00:00.000Z. Then 5432 5: every level is copied with spread, so base is untouched. The last line prints true: values taken from the source are shared, so wrap the result in structuredClone() if you need a fully independent copy. Libraries such as lodash's merge do the same job with more rules; check how they treat arrays, since lodash merges them index by index instead of replacing them.
5Which should you use?
| Method | Changes the original | Nested objects | Best for |
|---|---|---|---|
| { ...a, ...b } | No, new object | Replaced (shallow) | Almost everything |
| Object.assign({}, a, b) | No, new object | Replaced (shallow) | Older code, same result as spread |
| Object.assign(a, b) | Yes, writes into a | Replaced (shallow) | Updating an object you own |
| deepMerge(a, b) | No, new object | Merged key by key | Nested config and settings |
6Conditionally adding a property
Spread also answers the question of how to add a key only when a condition holds, without an if after the literal. Write ...(condition && { key: value }) inside the object literal. When the condition is true, the small object is spread in. When it is false, you spread false, and spreading false, null or undefined adds nothing (it doesn't throw, unlike spreading them into an array). The ternary form ...(condition ? { key } : {}) says the same thing more explicitly.
Output
Prints { limit: 20, search: 'shoes' } and { limit: 20, page: 3, includeDeleted: true }: the empty search and page 1 left no trace. Spreading the falsy values prints {}. The last two lines show the trap: retries && { retries } evaluates to 0, so a real zero is dropped and you get {}. Test what you actually mean, here retries !== undefined, and the key comes through as { retries: 0 }.
Frequently asked questions
When two objects have the same key, which value wins?
The later one. In { ...a, ...b } and Object.assign({}, a, b) properties are copied left to right, so b overwrites a. Put the object whose values should take priority last, for example { ...defaults, ...userSettings }. The key keeps the position where it first appeared, only its value changes.
What is the difference between Object.assign and the spread operator?
Both do a shallow, last-wins merge of own enumerable properties. { ...a, ...b } always creates a new object. Object.assign(target, ...sources) writes into target and returns it, so Object.assign(a, b) changes a; use Object.assign({}, a, b) for a copy. Object.assign also triggers setters on the target, while spread just defines new properties. Spread is ES2018; Object.assign is ES2015.
How do I deep merge nested objects in JavaScript?
There is no built-in deep merge. Spread and Object.assign replace a nested object instead of merging into it. Write a small recursive function that merges key by key when both sides hold a plain object and lets the source win otherwise, as in the deepMerge example above, or use a library function such as lodash merge. Decide up front how arrays should behave: replaced, concatenated or merged by index.
Why does a key set to undefined overwrite my default?
Because the key exists. Merging { host: "localhost", port: 8080 } with { host: undefined, port: 9090 } gives { host: undefined, port: 9090 }: spread copies own properties whatever their value. Drop undefined values first with Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)), and the same merge gives { host: 'localhost', port: 9090 }.
How do I add a property to an object only if a condition is true?
Spread a small object guarded by the condition inside the literal: { limit: 20, ...(search && { search }) }. When the condition is falsy you spread false, null or undefined, which adds nothing. Use the ternary ...(cond ? { key } : {}) if you prefer, and test explicitly (value !== undefined) when 0 or an empty string are legitimate values, because && drops them.
Is it safe to deep merge JSON from an untrusted source?
Be careful with the __proto__ key. JSON.parse creates it as a normal own property, and assigning it with result[key] = value sets the prototype of the result. The deepMerge above builds fresh objects, so a payload like {"__proto__": {"isAdmin": true}} does not touch Object.prototype, but the merged object does inherit isAdmin. When merging input you did not write, skip the keys __proto__, constructor and prototype.
Run it yourself
Open any of these in the full JavaScript editor: tweak, run, and share.
JavaScript playground