• Stars
    star
    336
  • Rank 125,564 (Top 3 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created about 6 years ago
  • Updated 6 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Merge objects & other types recursively. A simple & small integration.

Merge anything ๐Ÿฅก

Total Downloads Latest Stable Version

npm i merge-anything

Merge objects & other types recursively. Fully TypeScript supported! A simple & small integration.

Motivation

I 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... ๐Ÿ˜ž

I was looking for:

  • a simple merge function like Object.assign() but deep
  • supports merging of nested properties
  • supports TypeScript: the type of the result is what JS actually returns
  • supports symbols
  • supports enumerable & nonenumerable props
  • does not break special class instancesใ€€โ€ผ๏ธ

This 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!

merge-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". โ™ป๏ธ

Meet the family (more tiny utils with TS support)

Usage

  • Unlimited โ€” Merge will merge an unlimited amount of plain objects you pass as the arguments
  • Nested โ€” Nested objects are merged deeply (see example below)
  • 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)
import { merge } from 'merge-anything'

const starter = { name: 'Squirtle', types: { water: true } }
const newValues = { name: 'Wartortle', types: { fighting: true }, level: 16 }

const evolution = merge(starter, newValues, { is: 'cool' })
// returns {
//   name: 'Wartortle',
//   types: { water: true, fighting: true },
//   level: 16,
//   is: 'cool'
// }

TypeScript Support

In 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!

typescript support

The return type of the merge() function is usable as a TypeScript utility as well:

import type { Merge } from 'merge-anything'

type A1 = { name: string }
type A2 = { types: { water: boolean } }
type A3 = { types: { fighting: boolean } }

type Result = Merge<A1, [A2, A3]>

Rules

This package will recursively go through plain objects and merge the values onto a new object.

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"!

// all passed objects do not get modified
const a = { a: 'a' }
const b = { b: 'b' }
const c = { c: 'c' }
const result = merge(a, b, c)
// a === {a: 'a'}
// b === {b: 'b'}
// c === {c: 'c'}
// result === {a: 'a', b: 'b', c: 'c'}
// However, be careful with JavaScript object references with nested props. See below: A note on JavaScript object references

// arrays get overwritten
// (for "concat" logic, see Extensions below)
merge({ array: ['a'] }, { array: ['b'] }) // returns {array: ['b']}

// empty objects merge into objects
merge({ obj: { prop: 'a' } }, { obj: {} }) // returns {obj: {prop: 'a'}}

// but non-objects overwrite objects
merge({ obj: { prop: 'a' } }, { obj: null }) // returns {obj: null}

// and empty objects overwrite non-objects
merge({ prop: 'a' }, { prop: {} }) // returns {prop: {}}

merge-anything properly keeps special objects intact like dates, regex, functions, class instances etc.

However, it's very important you understand how to work around JavaScript object references. Please be sure to read #a note on JavaScript object references down below.

Concat arrays

The 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!

import { mergeAndConcat } from 'merge-anything'

mergeAndConcat(
  { nested: { prop: { array: ['a'] } } },
  { nested: { prop: { array: ['b'] } } }
)
// returns { nested: { prop: { array: ['a', 'b'] } } },

Compare Function when a value is merged

There 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.

For this case we use mergeAndCompare. Here is an example with a compare function that concatenates strings:

import { mergeAndCompare } from 'merge-anything'

function concatStrings(originVal, newVal, key) {
  if (typeof originVal === 'string' && typeof newVal === 'string') {
    // concat logic
    return `${originVal}${newVal}`
  }
  // always return newVal as fallback!!
  return newVal
}

mergeAndCompare(concatStrings, { name: 'John' }, { name: 'Simth' })
// returns { name: 'JohnSmith' }

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

A note on JavaScript object references

Be 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.

const original = { airport: { status: 'dep. ๐Ÿ›ซ' } }
const extraInfo = { airport: { location: 'Brussels' } }
const merged = merge(original, extraInfo)

// we change the status from departuring ๐Ÿ›ซ to landing ๐Ÿ›ฌ
merged.airport.status = 'lan. ๐Ÿ›ฌ'

// the `merged` value will be modified
// merged.airport.status === 'lan. ๐Ÿ›ฌ'

// However `original` value will also be modified!!
// original.airport.status === 'lan. ๐Ÿ›ฌ'

The key rule to remember is:

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

However, 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 library. This library also makes sure that special class instances do not break, so you can use it without fear of breaking stuff!

See below how we integrate 'copy-anything':

import { copy } from 'copy-anything'

const original = { airport: { status: 'dep. ๐Ÿ›ซ' } }
const extraInfo = { airport: { location: 'Brussels' } }
const merged = copy(merge(original, extraInfo))

// we change the status from departuring ๐Ÿ›ซ to landing ๐Ÿ›ฌ
merged.airport.status = 'lan. ๐Ÿ›ฌ'(merged.airport.status === 'lan. ๐Ÿ›ฌ')(
  // true
  // `original` won't be modified!
  original.airport.status === 'dep. ๐Ÿ›ซ'
) // true

You can then play around where you want to place the copy() function.

Copy Anything is also fully TypeScript supported!

Source code

It 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.

import { isPlainObject } from 'is-what'

function mergeRecursively(origin, newComer) {
  if (!isPlainObject(newComer)) return newComer
  // define newObject to merge all values upon
  const newObject = isPlainObject(origin)
    ? Object.keys(origin).reduce((carry, key) => {
        const targetVal = origin[key]
        if (!Object.keys(newComer).includes(key)) carry[key] = targetVal
        return carry
      }, {})
    : {}
  return Object.keys(newComer).reduce((carry, key) => {
    const newVal = newComer[key]
    const targetVal = origin[key]
    // early return when targetVal === undefined
    if (targetVal === undefined) {
      carry[key] = newVal
      return carry
    }
    // When newVal is an object do the merge recursively
    if (isPlainObject(newVal)) {
      carry[key] = mergeRecursively(targetVal, newVal)
      return carry
    }
    // all the rest
    carry[key] = newVal
    return carry
  }, newObject)
}

* Of course, there are small differences with the actual source code to cope with rare cases & extra features. The actual source code is here.

More Repositories

1

vuex-easy-firestore

Easy coupling of firestore and a vuex module. 2-way sync with 0 boilerplate!
JavaScript
234
star
2

is-what

JS type check (TypeScript supported) functions like `isPlainObject() isArray()` etc. A simple & small integration.
TypeScript
170
star
3

case-anything

camelCase, kebab-case, PascalCase... a simple integration with nano package size. (SMALL footprint!)
TypeScript
92
star
4

vuex-easy-access

Unified syntax for accessing your Vuex store through simple set() and get() functions + auto generate mutations
JavaScript
52
star
5

quasar-app-extension-draggable

A Quasar extension that makes elements draggable and movable with keyboard.
Vue
48
star
6

copy-anything

An optimised way to copy'ing (cloning) an Object or Array. A small and simple integration
TypeScript
40
star
7

filter-anything

A simple (TypeScript) integration of "pick" and "omit" to filter props of an object
TypeScript
31
star
8

quasar-ui-easy-forms

A Quasar extension to easily generate forms by only defining a "schema" object.
JavaScript
31
star
9

quasar-app-extension-swipe-to-close

A Quasar extension that allows you to close dialogs by swiping.
Vue
29
star
10

flatten-anything

Flatten objects and replace nested props with 'prop.subprop'. A simple and small integration.
TypeScript
25
star
11

find-and-replace-anything

Replace one val with another or all occurrences in an object recursively. A simple & small integration.
TypeScript
21
star
12

is-where

JS environment check functions like `isWebkit() isSafari() isBrowser() isNode()` etc. A simple & small integration.
TypeScript
19
star
13

quasar-ui-easy-tables

JavaScript
10
star
14

vuex-easy-firestore-sample-app

This is a sample app for vuex-easy-firestore
Vue
9
star
15

compare-anything

Compares objects and tells you which props are duplicate, and props are only present once.
TypeScript
9
star
16

remove-anything

An optimised way to remove any prop value (eg. undefined, empty objects, ...) from an object. A small and simple integration
TypeScript
8
star
17

map-anything

Array.map but for objects with good TypeScript support. A small and simple integration.
TypeScript
7
star
18

getorset-anything

Gets a value from a Map/Obj or sets an initial value when not found and returns that. TypeScript supported.
TypeScript
7
star
19

nestify-anything

Recreates an object from any `nested.props`. A simple and small integration.
TypeScript
5
star
20

check-anything

Checks anything from URLs to email addresses. A small and simple integration.
TypeScript
4
star
21

SwiftDataSugar

๐ŸŒฏ A collection of utilities that make it easier to work with SwiftData in a SwiftUI environment
Swift
4
star
22

sha-anything

A tiny TS utility to sha256 anything, including objects
TypeScript
3
star
23

SwiftVsTypeScript

A cheatsheet for those dabbling in both languages ๐Ÿ‘๐Ÿป
Vue
3
star
24

quasar-app-extension-draggable-demo

Demo for: A Quasar extension that makes elements draggable and movable with keyboard.
Vue
2
star
25

lucaban.com

Portal website lucaban.com
JavaScript
2
star
26

JustSugar

๐Ÿฐ JS-inspired Syntax Sugar on top of Swift to make working with Arrays/Strings/... easier
Swift
2
star
27

CaseAnything

๐Ÿซ Swift Case Conversions โ€” camelCase PascalCase UpperCamelCase kebab-case snake_case CONSTANT_CASE Train-Case Ada_Case COBOL-CASE Dot.notation Path/case Space case Capital Case lower case UPPER CASE
Swift
2
star
28

commafy-anything

Return a number as string with , or K. A simple and small integration
TypeScript
2
star
29

roll-anything

This is a very tiny dice util. ๐ŸŽฒ You can roll a dice with any amount of sides.
TypeScript
1
star
30

path-to-prop

Retrieves a property from an object based on a path.to.that.prop
TypeScript
1
star
31

MicroMaxOnAppleSilicon

โ™Ÿ๏ธ The ยต-Max C Chess engine to play Chess games. Built as multi-platform Swift Package for iOS, visionOS, macOS
C++
1
star
32

SwiftStringCatalogsPOC

A proof of concept that combines the String Catalog of a Swift Package together with a host app's.
Swift
1
star