Skip to content

Instantly share code, notes, and snippets.

@sladg
Created March 22, 2019 15:36
Show Gist options
  • Save sladg/dbe85b1794130ca9242079bc7962e355 to your computer and use it in GitHub Desktop.
Save sladg/dbe85b1794130ca9242079bc7962e355 to your computer and use it in GitHub Desktop.
Merging nested objects
import { assert } from 'chai'
import { mergeDeep as merge } from './'
describe('mergeDeep', () => {
it('should merge object properties without affecting any object', () => {
const obj1 = { a: 0, b: 1 }
const obj2 = { c: 2, d: 3 }
const obj3 = { a: 4, d: 5 }
const actual = { a: 4, b: 1, c: 2, d: 5 }
assert.deepEqual(merge({}, obj1, obj2, obj3), actual)
assert.notDeepEqual(actual, obj1)
assert.notDeepEqual(actual, obj2)
assert.notDeepEqual(actual, obj3)
})
it('should do a deep merge', () => {
const obj1 = { a: { b: 1, c: 1, d: { e: 1, f: 1 } } }
const obj2 = { a: { b: 2, d: { f: 'f' } } }
const result = merge(obj1, obj2)
assert.deepEqual(result, { a: { b: 2, c: 1, d: { e: 1, f: 'f' } } })
})
it('should not merge strings', () => {
const obj1 = { a: 'fooo' }
const obj2 = { a: { b: 2, d: { f: 'f' } } }
const obj3 = { a: 'bar' }
const actual = merge(obj1, obj2, obj3)
assert.deepEqual(actual.a, 'bar')
})
it('should clone objects during merge', () => {
const obj1 = { a: { b: 1 } }
const obj2 = { a: { c: 2 } }
const actual = merge({}, obj1, obj2)
assert.deepEqual(actual, { a: { b: 1, c: 2 } })
assert.notDeepEqual(actual.a, obj1.a)
assert.notDeepEqual(actual.a, obj2.a)
})
it('should not merge an objects into an array', () => {
const obj1 = { a: { b: 1 } }
const obj2 = { a: ['foo', 'bar'] }
const actual = merge({}, obj1, obj2)
assert.deepEqual(actual, { a: ['foo', 'bar'] })
})
it('should deep clone arrays during merge', () => {
const obj1 = { a: [1, 2, [3, 4]] }
const obj2 = { b: [5, 6] }
const actual = merge(obj1, obj2)
assert.deepEqual(actual.a, [1, 2, [3, 4]])
assert.deepEqual(actual.a[2], [3, 4])
assert.deepEqual(actual.b, obj2.b)
})
it('should not replace actual value with empty object', () => {
const obj1 = { a: {} }
const obj2 = { a: 5 }
const actualLeft = merge(obj1, obj2)
assert.deepEqual(actualLeft, { a: 5 })
})
it('should replace value with undefined if set', () => {
const obj1 = { a: 1024 }
const obj2 = { a: undefined }
const actualLeft = merge(obj1, obj2)
assert.deepEqual(actualLeft, { a: undefined })
const actualRight = merge(obj2, obj1)
assert.deepEqual(actualRight, { a: 1024 })
})
it('should copy source properties', () => {
assert.equal(merge({ test: true }).test, true)
})
})
// tslint:disable
const isObject = (item) => item && typeof item === 'object' && !Array.isArray(item)
const mergeNew = (target, source) => {
// Nothing to look at, go away
if (target === undefined) return source
if (source === undefined) return target
// We know how to handle arrays! We don't merge those.
if (Array.isArray(source)) return source
// Nothing to merge here
// @TODO: we might want to merge arrays here later on
if (!isObject(source) || !isObject(target)) return source
// Replace empty object with whatever we have insted of it
if (!Object.entries(target || {}).length) return source
const entries = [...Object.entries(target || {}), ...Object.entries(source || {})]
const isHardToMerge = entries.reduce((acc, val) => (acc ? acc : isObject(val[1])), false)
// Let's do it!
if (!isHardToMerge) return { ...target, ...source }
// So it's hard, okay then
return entries
.map(([key]) => key)
.reduce((acc, key) => {
// Check if we actually want to have undefined here
return source.hasOwnProperty(key) && source[key] === undefined
? { ...acc, [key]: source[key] }
: { ...acc, [key]: mergeNew(target[key], source[key]) }
}, {})
}
export const mergeDeep = (target, ...rest) => rest.reduce((prev, next) => mergeNew(prev, next), target)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment