UNPKG

9.99 kBMarkdownView Raw
1# Merge anything 🥡
2
3<a href="https://www.npmjs.com/package/merge-anything"><img src="https://img.shields.io/npm/v/merge-anything.svg" alt="Total Downloads"></a>
4<a href="https://www.npmjs.com/package/merge-anything"><img src="https://img.shields.io/npm/dw/merge-anything.svg" alt="Latest Stable Version"></a>
5
6```
7npm i merge-anything
8```
9
10Merge objects & other types recursively. Fully **TypeScript** supported! A simple & small integration.
11
12## Motivation
13
14I created this package because I tried a lot of similar packages that do merging/deepmerging/recursive object assign etc. But all had its quirks, and _all of them break things they are not supposed to break_... 😞
15
16I was looking for:
17
18- a simple merge function like `Object.assign()` but deep
19- supports merging of nested properties
20- supports TypeScript: the type of the result is what JS actually returns
21- supports symbols
22- supports enumerable & nonenumerable props
23- **does not break special class instances** ‼️
24
25This last one is crucial! In JavaScript almost everything is _an object_, sure, but I don't want a merge function trying to merge eg. two `new Date()` instances! So many libraries use custom classes that create objects with special prototypes, and such objects all break when trying to merge them. So we gotta be careful!
26
27merge-anything will merge objects and nested properties, but only as long as they're "plain objects". As soon as a sub-prop is not a "plain object" and has a special prototype, it will copy that instance over "as is". ♻️
28
29## Meet the family (more tiny utils with TS support)
30
31- [is-what 🙉](https://github.com/mesqueeb/is-what)
32- [is-where 🙈](https://github.com/mesqueeb/is-where)
33- [merge-anything 🥡](https://github.com/mesqueeb/merge-anything)
34- [check-anything 👁](https://github.com/mesqueeb/check-anything)
35- [remove-anything ✂️](https://github.com/mesqueeb/remove-anything)
36- [getorset-anything 🐊](https://github.com/mesqueeb/getorset-anything)
37- [map-anything 🗺](https://github.com/mesqueeb/map-anything)
38- [filter-anything ⚔️](https://github.com/mesqueeb/filter-anything)
39- [copy-anything 🎭](https://github.com/mesqueeb/copy-anything)
40- [case-anything 🐫](https://github.com/mesqueeb/case-anything)
41- [flatten-anything 🏏](https://github.com/mesqueeb/flatten-anything)
42- [nestify-anything 🧅](https://github.com/mesqueeb/nestify-anything)
43
44## Usage
45
46- Unlimited — Merge will merge an unlimited amount of plain objects you pass as the arguments
47- Nested — Nested objects are merged deeply (see example below)
48- No modification — Merge always returns a new object without modifying the original, but does keep object/array references for nested props (see [#A note on JavaScript object references](#a-note-on-javascript-object-references))
49
50```js
51import { merge } from 'merge-anything'
52
53const starter = { name: 'Squirtle', types: { water: true } }
54const newValues = { name: 'Wartortle', types: { fighting: true }, level: 16 }
55
56const evolution = merge(starter, newValues, { is: 'cool' })
57// returns {
58// name: 'Wartortle',
59// types: { water: true, fighting: true },
60// level: 16,
61// is: 'cool'
62// }
63```
64
65## TypeScript Support
66
67In the example above, if you are using TypeScript, and you hover over `evolution`, you can actually see the type of your new object right then and there. This is very powerful, because you can merge things, and without needing `any`, TypeScript will know exactly how your newly merged objects look!
68
69![typescript support](https://raw.githubusercontent.com/mesqueeb/merge-anything/master/.github/typescript-support.png)
70
71The return type of the `merge()` function is usable as a TypeScript utility as well:
72
73```ts
74import type { Merge } from 'merge-anything'
75
76type A1 = { name: string }
77type A2 = { types: { water: boolean } }
78type A3 = { types: { fighting: boolean } }
79
80type Result = Merge<A1, [A2, A3]>
81```
82
83## Rules
84
85This package will recursively go through plain objects and merge the values onto a new object.
86
87> Please note that this package recognises special JavaScript objects like class instances. In such cases it will not recursively merge them like objects, but assign the class onto the new object "as is"!
88
89```js
90// all passed objects do not get modified
91const a = { a: 'a' }
92const b = { b: 'b' }
93const c = { c: 'c' }
94const result = merge(a, b, c)
95// a === {a: 'a'}
96// b === {b: 'b'}
97// c === {c: 'c'}
98// result === {a: 'a', b: 'b', c: 'c'}
99// However, be careful with JavaScript object references with nested props. See below: A note on JavaScript object references
100
101// arrays get overwritten
102// (for "concat" logic, see Extensions below)
103merge({ array: ['a'] }, { array: ['b'] }) // returns {array: ['b']}
104
105// empty objects merge into objects
106merge({ obj: { prop: 'a' } }, { obj: {} }) // returns {obj: {prop: 'a'}}
107
108// but non-objects overwrite objects
109merge({ obj: { prop: 'a' } }, { obj: null }) // returns {obj: null}
110
111// and empty objects overwrite non-objects
112merge({ prop: 'a' }, { prop: {} }) // returns {prop: {}}
113```
114
115merge-anything properly keeps special objects intact like dates, regex, functions, class instances etc.
116
117However, it's **very important** you understand how to work around JavaScript object references. Please be sure to read [#a note on JavaScript object references](#a-note-on-javascript-object-references) down below.
118
119## Concat arrays
120
121The default behaviour is that arrays are overwritten. You can import `mergeAndConcat` if you need to concatenate arrays. But don't worry if you don't need this, this library is tree-shakable and won't import code you don't use!
122
123<!-- prettier-ignore-start -->
124```js
125import { mergeAndConcat } from 'merge-anything'
126
127mergeAndConcat(
128 { nested: { prop: { array: ['a'] } } },
129 { nested: { prop: { array: ['b'] } } }
130)
131// returns { nested: { prop: { array: ['a', 'b'] } } },
132```
133<!-- prettier-ignore-end -->
134
135## Compare Function when a value is merged
136
137There might be times you need to tweak the logic when two things are merged. You can provide your own custom function that's triggered every time a value is overwritten.
138
139For this case we use `mergeAndCompare`. Here is an example with a compare function that concatenates strings:
140
141```js
142import { mergeAndCompare } from 'merge-anything'
143
144function concatStrings(originVal, newVal, key) {
145 if (typeof originVal === 'string' && typeof newVal === 'string') {
146 // concat logic
147 return `${originVal}${newVal}`
148 }
149 // always return newVal as fallback!!
150 return newVal
151}
152
153mergeAndCompare(concatStrings, { name: 'John' }, { name: 'Simth' })
154// returns { name: 'JohnSmith' }
155```
156
157> Note for TypeScript users. The type returned by this function might not be correct. In that case you have to cast the result to your own provided interface
158
159## A note on JavaScript object references
160
161Be careful for JavaScript object reference. Any property that's nested will be reactive and linked between the original and the merged objects! Down below we'll show how to prevent this.
162
163```js
164const original = { airport: { status: 'dep. 🛫' } }
165const extraInfo = { airport: { location: 'Brussels' } }
166const merged = merge(original, extraInfo)
167
168// we change the status from departuring 🛫 to landing 🛬
169merged.airport.status = 'lan. 🛬'
170
171// the `merged` value will be modified
172// merged.airport.status === 'lan. 🛬'
173
174// However `original` value will also be modified!!
175// original.airport.status === 'lan. 🛬'
176```
177
178The key rule to remember is:
179
180> Any property that's nested more than 1 level without an overlapping parent property will be reactive and linked in both the merge result and the source
181
182However, **there is a really easy solution**. We can just copy the merge result to get rid of any reactivity. For this we can use the [copy-anything](https://github.com/mesqueeb/copy-anything) library. This library also makes sure that _special class instances do not break_, so you can use it without fear of breaking stuff!
183
184See below how we integrate 'copy-anything':
185
186```js
187import { copy } from 'copy-anything'
188
189const original = { airport: { status: 'dep. 🛫' } }
190const extraInfo = { airport: { location: 'Brussels' } }
191const merged = copy(merge(original, extraInfo))
192
193// we change the status from departuring 🛫 to landing 🛬
194merged.airport.status = 'lan. 🛬'(merged.airport.status === 'lan. 🛬')(
195 // true
196 // `original` won't be modified!
197 original.airport.status === 'dep. 🛫'
198) // true
199```
200
201You can then play around where you want to place the `copy()` function.
202
203Copy Anything is also fully TypeScript supported!
204
205## Source code
206
207It is literally just going through an object recursively and assigning the values to a new object like below. However, it's wrapped to allow extra params etc. The code below is the basic integration, that will make you understand the basics how it works.
208
209```js
210import { isPlainObject } from 'is-what'
211
212function mergeRecursively(origin, newComer) {
213 if (!isPlainObject(newComer)) return newComer
214 // define newObject to merge all values upon
215 const newObject = isPlainObject(origin)
216 ? Object.keys(origin).reduce((carry, key) => {
217 const targetVal = origin[key]
218 if (!Object.keys(newComer).includes(key)) carry[key] = targetVal
219 return carry
220 }, {})
221 : {}
222 return Object.keys(newComer).reduce((carry, key) => {
223 const newVal = newComer[key]
224 const targetVal = origin[key]
225 // early return when targetVal === undefined
226 if (targetVal === undefined) {
227 carry[key] = newVal
228 return carry
229 }
230 // When newVal is an object do the merge recursively
231 if (isPlainObject(newVal)) {
232 carry[key] = mergeRecursively(targetVal, newVal)
233 return carry
234 }
235 // all the rest
236 carry[key] = newVal
237 return carry
238 }, newObject)
239}
240```
241
242\* Of course, there are small differences with the actual source code to cope with rare cases & extra features. The actual source code is [here](https://github.com/mesqueeb/merge-anything/blob/master/src/merge.ts).