d
Amit DhamuSoftware Engineer
 

Recursively deep merging objects

7 minute read 00000 views

This is an alternative to lodash.merge.

type Props = Record<string, any>

const deepMerge = (target: Props, ...sources: Props[]): Props => {
  if (!sources.length) {
    return target
  }

  Object.entries(sources.shift() ?? []).forEach(([key, value]) => {
    if (value) {
      if (!target[key]) {
        Object.assign(target, { [key]: {} })
      }

      if (
        value.constructor === Object ||
        (value.constructor === Array &&
          value.find(v => v.constructor === Object))
      ) {
        deepMerge(target[key], value)
      } else if (value.constructor === Array) {
        Object.assign(target, {
          [key]: value.find(v => v.constructor === Array)
            ? target[key].concat(value)
            : [...new Set([...target[key], ...value])],
        })
      } else {
        Object.assign(target, { [key]: value })
      }
    }
  })

  return target
}

Test Cases

Shallow merge

deepMerge(
  {
    a: '__PROPERTY_A__',
    b: '__PROPERTY_B__',
  },
  {
    c: '__PROPERTY_C__',
  }
)

// Returns
{
  a: '__PROPERTY_A__',
  b: '__PROPERTY_B__',
  c: '__PROPERTY_C__',
}

Identical properties stay the same

deepMerge(
  {
    a: '__PROPERTY_A__',
    b: '__PROPERTY_B__',
  },
  {
    b: '__PROPERTY_B__',
    c: '__PROPERTY_C__',
  }
)

// Returns
{
  a: '__PROPERTY_A__',
  b: '__PROPERTY_B__',
  c: '__PROPERTY_C__',
}

Flat arrays are joined with duplicates removed

deepMerge(
  {
    items: ['h', 'b'],
  },
  {
    items: ['a', 'b'],
  }
)

// Returns
{
  items: ['h', 'b', 'a'],
}

Nested arrays are joined together

deepMerge(
  {
    items: [
      [1, 2, 3],
      [4, 5, 6],
    ],
  },
  {
    items: [[4, 5, 6, 7, 8]],
  }
)

// Returns
{
  items: [
    [1, 2, 3],
    [4, 5, 6],
    [4, 5, 6, 7, 8],
  ],
}

Merging arrays of objects

deepMerge(
  {
    name: '__NAME__',
    orders: [
      {
        orderId: 12345,
        item: 'Silk gloves',
      },
      {
        orderId: 67891,
        item: 'Black jeans',
      },
    ],
  },
  {
    orders: [
      {
        orderId: 12345,
        item: 'Silk gloves',
        price: 99.99,
      },
    ],
  }
)

// Returns
{
  name: '__NAME__',
  orders: [
    {
      item: 'Silk gloves',
      orderId: 12345,
      price: 99.99,
    },
    {
      item: 'Black jeans',
      orderId: 67891,
    },
  ],
}

Merging nested objects

deepMerge(
  {
    version: 1,
    a: '__PROPERTY_A__',
    b: {
      a: '__PROPERTY_A__',
    },
    c: {
      a: '__PROPERTY_A__',
      e: '__PROPERTY_E__',
    },
  },
  {
    nested: true,
    date: new Date('2021-05-05').toISOString(),
    a: '__PROPERTY_A__',
    b: {
      b: '__PROPERTY_B__',
    },
    c: {
      b: '__PROPERTY_B__',
      d: '__PROPERTY_D__',
      f: {
        a: '__PROPERTY_A__',
        b: '__PROPERTY_B__',
        c: '__PROPERTY_C__',
        d: {
          a: '__PROPERTY_A__',
        },
      },
    },
  }
)

// Returns
{
  nested: true,
  version: 1,
  date: '2021-05-05T00:00:00.000Z',
  a: '__PROPERTY_A__',
  b: {
    a: '__PROPERTY_A__',
    b: '__PROPERTY_B__',
  },
  c: {
    a: '__PROPERTY_A__',
    b: '__PROPERTY_B__',
    d: '__PROPERTY_D__',
    e: '__PROPERTY_E__',
    f: {
      a: '__PROPERTY_A__',
      b: '__PROPERTY_B__',
      c: '__PROPERTY_C__',
      d: {
        a: '__PROPERTY_A__',
      },
    },
  },
}