# ya-merge

> Yet another deep merge function

Latest version **1.1.0** (published 2026-09-24) · ISC license · 0 weekly downloads

## Install

```sh
npm install ya-merge
pnpm add ya-merge
yarn add ya-merge
bun add ya-merge
```

## Health

**Score 60/100 (C)** — status: active.

Positive: esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads; no types.

## Facts

| | |
|---|---|
| Version | 1.1.0 |
| Published | 2026-09-24 |
| First published | 2021-09-19 |
| Weekly downloads | 0 |
| License | ISC |
| TypeScript types | none |
| Module format | ESM + CommonJS |
| Dependencies | 0 |
| Unpacked size | 10.5 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 0 |
| Author | Petro Borshchahivskyi |
| Maintainers | liksu |
| Keywords | merge, deep, deep-merge, deepmerge, merge-deep, recursive, recursive-merge, merging, extend, assign, copy, object, objects, array, nested, defaults, config |

## Links

- npm: https://www.npmjs.com/package/ya-merge
- Repository: https://github.com/Liksu/ya-merge
- Homepage: https://github.com/Liksu/ya-merge#readme
- Issues: https://github.com/Liksu/ya-merge/issues
- npm.io page: https://npm.io/package/ya-merge

## Alternatives

- [jsforce](https://npm.io/package/jsforce.md) — 851.2K weekly downloads
- [react-native-qrcode-svg](https://npm.io/package/react-native-qrcode-svg.md) — 693.5K weekly downloads
- [@salesforce/plugin-data](https://npm.io/package/@salesforce/plugin-data.md) — 394.9K weekly downloads
- [@backstage/plugin-search-common](https://npm.io/package/@backstage/plugin-search-common.md) — 308.5K weekly downloads
- [@chain-registry/types](https://npm.io/package/@chain-registry/types.md) — 38.4K weekly downloads

## Recent versions

- 1.1.0 (latest) — 2026-09-24
- 1.0.1 — 2021-09-19
- 1.0.0 — 2021-09-19

## README

# ya-merge

Yet another deep merge function.

`merge(...objects)` folds any number of values left to right and returns a **new** object; the rightmost value always wins on conflicts. Unlike `Object.assign` or `{...a, ...b}`, nested objects are merged recursively instead of being replaced wholesale.

```javascript
import merge from "ya-merge";

merge({a: {x: 1}}, {a: {y: 2}})       // { a: { x: 1, y: 2 } }
Object.assign({a: {x: 1}}, {a: {y: 2}}) // { a: { y: 2 } }   <- nested x is lost
```

## Install

```bash
npm install --save ya-merge
```

The package is ESM-only (`"type": "module"`). In CommonJS use a dynamic import:

```javascript
const {default: merge} = await import("ya-merge")
```

### From a CDN

The published package is a plain ES module with no dependencies and no build step, so every npm CDN serves it as is — no bundler, no install:

```html
<script type="module">
    import merge from "https://unpkg.com/ya-merge"

    console.log(merge({a: {x: 1}}, {a: {y: 2}}))
</script>
```

The same file is available from the other usual mirrors:

| CDN | URL |
| --- | --- |
| unpkg | `https://unpkg.com/ya-merge` |
| jsDelivr | `https://cdn.jsdelivr.net/npm/ya-merge` |
| esm.sh | `https://esm.sh/ya-merge` |

## Usage

```javascript
import merge from "ya-merge";

const defaults = {
    server: {host: "localhost", port: 8080},
    retries: 3,
    tags: ["default"],
}

const config = {
    server: {port: 3000},
    tags: ["prod"],
}

merge(defaults, config)
// {
//   server: {host: "localhost", port: 3000},
//   retries: 3,
//   tags: ["prod"],
// }
```

Any number of arguments is supported and they are applied in order:

```javascript
merge({a: 1}, {b: 2}, {c: 3})          // { a: 1, b: 2, c: 3 }
merge({x: 0}, {x: 1}, {x: 2})          // { x: 2 }
```

## Merge rules

| Left (`a`) | Right (`b`) | `merge(a, b)` |
| --- | --- | --- |
| plain object | plain object | new object with the union of keys, values merged recursively |
| array | array | merged **element by element**, length is `max(a.length, b.length)` |
| array | anything else | `b` is merged into **every** element of `a` |
| anything else | array | `a` is merged into **every** element of `b` |
| anything | `undefined` | `a` is kept |
| anything | `null` | `null` — `b` wipes the branch instead of merging into it |
| `undefined`/`null`/`0`/`""`/`false` | anything | `b` is returned as is |
| anything | primitive or function | `b` is returned as is |
| different `typeof` | | `b` is returned as is |
| anything | `Date`, `Map`, `Set`, `WeakMap`, `WeakSet`, DOM `Element` | `b` is returned **by reference**, never merged into |

The rules apply to the fold as a whole, so a rightmost value that replaces rather than merges wins over everything before it:

```javascript
merge({a: 1}, {b: 2}, 7)               // 7
merge({a: 1}, false)                   // false
merge({a: 1}, {b: 2}, new Date(0))     // the Date
```

### Arrays are merged by index, not concatenated

```javascript
merge([1, 2, 3], [9])                  // [9, 2, 3]
merge([1, 2], [9, 8, 7])               // [9, 8, 7]
merge([{a: 1}, {a: 2}], [{b: 1}, {b: 2}])
// [ {a: 1, b: 1}, {a: 2, b: 2} ]
```

Use a hole (or `undefined`) to keep an element untouched:

```javascript
merge([{a: 1}, {a: 2}], [undefined, {b: 2}])
// [ {a: 1}, {a: 2, b: 2} ]
```

If you want concatenation, do it yourself: `merge(a, b)` never appends.

### Mixing an array with a non-array broadcasts

The non-array side is merged into each element of the array. This is handy for stamping shared fields onto a collection:

```javascript
merge([{id: 1}, {id: 2}], {active: true})
// [ {id: 1, active: true}, {id: 2, active: true} ]

merge({active: true}, [{id: 1}, {id: 2}])
// [ {active: true, id: 1}, {active: true, id: 2} ]
```

### Built-in objects are atomic

`Date`, `Map`, `Set`, `WeakMap`, `WeakSet` and DOM `Element` instances are treated as opaque values — the right-hand one replaces the left-hand one **by reference**, it is not copied or merged into:

```javascript
const d = new Date("2021-01-01")
merge({at: new Date("2020-01-01")}, {at: d}).at === d   // true
```

The same holds for class instances whose `toString()` differs from each other — they are replaced rather than merged.

## Design notes

`merge` is deliberately small, and these are the trade-offs that come with it.

**Not a deep clone.** Sub-trees that exist on only one side are carried over by reference, so the result shares structure with the inputs:

```javascript
const a = {keep: {deep: 1}}
const result = merge(a, {other: 2})
result.keep === a.keep      // true — mutating one mutates the other
```

The arguments themselves are never mutated, and branches present on both sides are rebuilt as new objects.

If you need a result that shares nothing with its inputs, wrap the merge in `structuredClone`:

```javascript
const isolated = structuredClone(merge(a, b))
isolated.keep === a.keep      // false
```

That also upgrades the atomics — `Date`, `Map`, `Set` and `RegExp` come out as real clones instead of being carried over by reference. In exchange it throws `DataCloneError` on function values and on `WeakMap`/`WeakSet`, so it only fits data that is structured-cloneable.

**Prototypes are dropped.** The result of merging two objects is always a plain object, so class instances lose their prototype and methods:

```javascript
class Point { constructor(x) { this.x = x } len() { return this.x } }
const p = merge(new Point(1), {y: 2})
p.constructor.name     // "Object"
p.len                  // undefined
```

**Only own enumerable string keys are copied.** Symbol keys and non-enumerable properties are dropped, and getters are evaluated and flattened into plain values.

**Peer circular references are not supported.** `merge` keeps no record of what it has already visited, so a cycle that both arguments reach along the same chain of keys makes the recursion loop until the stack runs out:

```javascript
const a = {n: 1}; a.self = a
const b = {n: 2}; b.self = b

merge(a, b)     // RangeError: Maximum call stack size exceeded
```

A cycle on only one side is safe, because that branch has no counterpart to descend into and is carried over by reference:

```javascript
const a = {n: 1}; a.self = a

merge(a, {m: 2})     // { n: 1, self: <the original a>, m: 2 }
merge({m: 2}, a)     // { m: 2, n: 1, self: <the original a> }
```

Two cycles that sit at different keys are safe for the same reason — the recursion runs out of pairs before it runs out of stack. Only cycles that meet make it loop:

```javascript
const a = {n: 1}; a.self = a
const b = {n: 2}; b.other = b

merge(a, b)     // { n: 2, self: <a>, other: <b> }
```

If you need to merge such data, break the cycle first, or reach for a library that tracks visited pairs — `lodash.merge` does.

## API

```typescript
merge(...objects: any[]): any
```

Returns the merged value. With no arguments returns `undefined`; with a single argument returns it unchanged. Never mutates its arguments.

## License

ISC © Petro Borshchahivskyi

---
_Source: https://npm.io/package/ya-merge · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
