TSR
TSR

Reputation: 20647

How to update an object with another object in JavaScript

I am trying to create a function that takes a target json object a source json object (the update) The rule is that the target must be updated by the source. If the key are the same, the value of the target is overwritten. If the key contains a dot . we consider it as a nested update (example 3) If the update key does not exist in the target, we create it.

Example 1:

var target = {
    foo: 1,
}
var source = {
    foo: 22,
}
var output = {
    foo: 22
}

Example 2:

var target = {
    foo: {
        bar: 1
    },
}
var source = {
    foo: {
        baz: 999
    },
}
var output = {
    foo: {
        baz: 999
    }
}

Example 3:

var target = {
    foo: {
        bar: 1,
        baz: 99,
    },
}
var source = {
    'foo.bar': 22,
}
var output = {
    foo: {
        bar: 22,
        baz: 99
    }
}

Example 4 (Array)

We assume that numbers are invalid key for the source. For example, the below source will never occur

// this will never happen
let source = {
        1: 'something'
}

However, update can update the value of an array at a specif index using the dot . method

const source = {
    foo: [0, 1,2,3, {four: 4}]
}
const target = {
    'foo.0': 'zero',
    'foo.4.four': 'four',
    'foo.4.five': 5,
}
const output = {
    foo: ['zero', 1, 2, 3, {four: 'four', five: 5}]
}

Hence array can not change in size using the dot update, to change the size, we need to overwrite the entire array

My function below works perfectly fine with the Example 1 but I cannot get it to work with example 2 and 3.

Instead I obtain:

var example2 = {
    bar: 1,
    baz: 999,
}


var example3 = {
    foo: {
        bar: 1,
        baz: 99,
    },
    'foo.bar': 22,
}

function deepMerge(target, source) {
    Object.entries(source).forEach(([key, value]) => {
        if (value && typeof value === 'object') {
            deepMerge(target[key] = target[key] || {}, value);
            return;
        }
        target[key] = value;
    });
    return target;
}

Upvotes: 1

Views: 347

Answers (3)

Phil
Phil

Reputation: 165069

I would perform the following actions

  1. Iterate all source entries (key / value pairs)
  2. Split the key into an array, separate by .. For non path-like keys, this will just be the key itself
  3. Find or create inner objects by path and then assign the new value

// utility function
const isObjectOrArray = obj => typeof obj === 'object' && obj !== null

const deepMerge = (target, source) => {
  // create a shallow copy because who wants to mutate source data
  const result = { ...target }
  
  // Iterate source entries
  Object.entries(source).forEach(([ path, val ]) => {
    const segments = path.split('.')
    // save the last segment to assign the value
    const last = segments.pop()
    
    // Find or create the deepest object by path
    const deepest = segments.reduce((obj, segment) => {
      // create objects if they aren't already
      if (!isObjectOrArray(obj[segment])) {
        // perhaps check if segment is a number and create an array instead
        // ¯\_(ツ)_/¯ I'm tired
        obj[segment] = {}
      }
      return obj[segment]
    }, result)
    
    // write the new value
    deepest[last] = val
  })
  return result
}

var target = {
  abc: 'abc',
  bar: {
    baz: 1
  },
  foo: {
    bar: 1,
    baz: 99,
  },
  nums: [0, 1,2,3, {four: 4}]
}
var source = {
  'foo.bar': 22,
  abc: 'def',
  'nums.0': 'zero',
  'nums.4.four': 'four',
  'nums.4.five': 5,
  bar: {
    bar: 'bar!'
  },
  'brand.new.object': 'value'
}

const output = deepMerge(target, source)
console.info(output)
.as-console-wrapper {max-height: none !important; top: 0;}

Upvotes: 1

Fahd Lihidheb
Fahd Lihidheb

Reputation: 710

This should works for every example you provided:

function deepMerge(target: any, source: any) {
    Object.keys(source).forEach((key: string) => {
        if (key.includes('.')) {
            const nested: any = {}
            nested[key.split('.')[1]] = source[key]
            target[key.split('.')[0]] = deepMerge(target[key.split('.')[0]], nested)
            delete source[key]
        }
    });
    return { ...target, ...source }
}

Upvotes: 1

Gurwinder
Gurwinder

Reputation: 509

Here if you break {'foo.bar': 22} into { foo: {bar: 22}} before giving it to input as a source you should get expected result.

function breakItApart(obj) {
  Object.entries(obj).forEach(([key, value]) => {

        [firstKey, anotherKey] = key.split('.')
        if (anotherKey) {
            obj[firstKey] = obj[firstKey] || {};
            obj[firstKey][anotherKey] = value;
            delete obj[key];
        }
    });
    return obj; 
}

This depends on the constraint that you would only get one dot in a key but you should be able extend this method for multiple dots.

var target = {
    foo: {
        bar: 1,
        baz: 99,
    },
}
var source = {
    'foo.bar': 22,
}

const result = deepMerge(target, breakItApart(source));
console.log(result); 
>> { foo: { bar: 22, baz: 99 } }

Upvotes: 0

Related Questions