How to identify changed property values when comparing two nested data structures?

171 views Asked by At

I'm working on a TypeScript project where I need to compare two objects with potentially nested structures and identify the fields that have changed between them. For instance, consider an old object oldObject with fields like name, age, city, friends (an array), and possibly nested objects within. Now, I have a new object newObject with the same structure, but with potentially updated values for some fields.

Here's an example:

const oldObject = { name: 'John', age: 30, city: 'New York', friends: ['ali', 'qasim'], def: { 'a': 1, 'b': 2 } };
const newObject = { name: 'John', age: 35, city: 'New York', friends: ['ali', 'haider'] };

In this scenario, I need to be able to identify and extract the changed fields between oldObject and newObject, along with their new values, retaining the nested structure.

I've attempted a comparison function, but it's not correctly handling nested structures and arrays. How can I implement a solution in TypeScript that accurately identifies the changed fields while preserving the nested structure of the objects?

  • I'm looking for a TypeScript function or approach that can accurately identify changed fields between two objects with nested structures.
  • The function should correctly handle nested objects and arrays, identifying changes within them.
  • The solution should be efficient and scalable for objects of varying sizes and complexities.
  • Any insights or improvements on the provided sample function would be greatly appreciated.
  • Clear explanations and examples would be helpful for better understanding and implementation.
3

There are 3 answers

3
TAYYAB-IT On BEST ANSWER

To accurately identify and compare changed fields in TypeScript objects with nested structures, you can utilize a recursive approach. Here's how you can implement a function to achieve this:

  function findChangedFields(oldObj: any, newObj: any): { old: any, new: any } {
const changedFields: { old: any, new: any } = { old: {}, new: {} };

for (const key in oldObj) {
    if (oldObj.hasOwnProperty(key)) {
        if (!newObj.hasOwnProperty(key)) {
            changedFields.old[key] = oldObj[key];
            changedFields.new[key] = undefined; // Field does not exist in new object
        } else if (Array.isArray(oldObj[key]) && Array.isArray(newObj[key])) {
            if (JSON.stringify(oldObj[key]) !== JSON.stringify(newObj[key])) {
                changedFields.old[key] = oldObj[key];
                changedFields.new[key] = newObj[key];
            }
        } else if (typeof oldObj[key] === 'object' && typeof newObj[key] === 'object') {
            const nestedChanges = findChangedFields(oldObj[key], newObj[key]);
            if (Object.keys(nestedChanges.old).length > 0 || Object.keys(nestedChanges.new).length > 0) {
                changedFields.old[key] = nestedChanges.old;
                changedFields.new[key] = nestedChanges.new;
            }
        } else {
            if (oldObj[key] !== newObj[key]) {
                changedFields.old[key] = oldObj[key];
                changedFields.new[key] = newObj[key];
            }
        }
    }
}

// Check for new keys in newObj
for (const key in newObj) {
    if (newObj.hasOwnProperty(key) && !oldObj.hasOwnProperty(key)) {
        changedFields.old[key] = undefined; // Field does not exist in old object
        changedFields.new[key] = newObj[key];
    }
}

return changedFields;
}

// Example usage:
const oldObject = { name: 'John', age: 30, city: 'New York', friends: ['ali', 'qasim'], def: { 'a': 1, 'b': 3 }, ng: { a: { d: 1 } } };
const newObject = { name: 'John', age: 35, city: 'New York', friends: ['ali', 'haider'], def: {}, ng: { a: {} } };

const changedFields = findChangedFields(oldObject, newObject);
console.log(changedFields);

This function recursively traverses through the objects and arrays to compare their elements. If a difference is found, it records the changed field along with its new value in the changedFields object.

You can test this function with your sample objects oldObject and newObject to see the changed fields along with their new values.

1
rahim sayyad On

const oldObject = { name: 'John', age: 30, city: 'New York', friends: ['ali', 'qasim'], def: { 'a': 1, 'b': 2 } };

const newObject = { name: 'John', age: 35, city: 'New York', friends: ['ali', 'haider'] };
let oldobj = Object.keys(oldObject)
console.log(oldobj)
let newobj = Object.keys(newObject)
console.log(newobj)
let result = oldobj.filter(x => !newobj.includes(x));

if(result.length> 0){
    console.log(oldObject.def)
}

// finding keys in 2 arrays . and checking keys are same in both araays or not

0
Peter Seliger On

A feasibly approach for tackling the OP's problem could be based on serializing any given data-structure into a keypath based map/index.

When comparing and diffing two data-structures one would first create a Map based serialized variant of each structure.

Then one easily can create a Set instance of each created map which holds just the keypath strings of each related, serialized data-structure.

By making use of the new Set methods intersection and difference (utilizing the js-core implementations of the latter two) one retrieves specific sets of keypath strings, like overlapping keypaths and keypaths which are unique to each data-structure.

Having reached that point it is pretty easy to create a diff object from all the created maps and sets, which is ...

  • computing a changed diff-entry which points out different values at one and the same keypath.

  • creating a deleted diff-entry which features keypath based key-value pairs that are unique to the provided recent data object.

  • creating an added diff-entry which features keypath based key-value pairs that are unique to the provided current data object.

const oldObject = {
  name: 'John', age: 30,
  friends: ['ali', 'qasim', { foo: 'Foo', 'foo bar': { baz: 'Baz', biz: 'Biz' } }],
};
const newObject = {
  name: 'John', age: 35, city: 'New York',
  friends: ['ali', 'haider', { 'foo bar': { baz: 'BAZ'}, foo: 'Foo' }],
};

const diff = diffDataValuesByKeypath(oldObject, newObject);

console.log({ oldObject, newObject, diff });
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/3.36.0/minified.js"></script>

<script>
function concatKeypath(keypath, key) {

  // - test for a key which can be used with dot chaining.
  // - see ... [https://regex101.com/r/tew8gr/1]
  return (/^[_$\p{L}][_$\p{L}\p{N}]*$/u).test(key)

    // - concat a dot chained key.
    ? (!!keypath && `${ keypath }.${ key }` || key)

    // - concat a quoted key with square bracket notation.
    : `${ keypath }["${ key }"]`;
}

function mapDataStructure(value, keypath = '', map = new Map) {
  if (Array.isArray(value)) {
    value
      .forEach((item, idx) =>

        mapDataStructure(item, `${ keypath }[${ idx }]`, map)
      );
  } else if (value && typeof value === 'object') {
    Object
      .entries(value)
      .forEach(([key, val]) =>

        mapDataStructure(val, concatKeypath(keypath, key), map)
      );
  } else {
    map.set(keypath, value);
  }
  return map;
}

function diffDataValuesByKeypath(recentData, currentData) {
  const recentMap = mapDataStructure(recentData);
  const currentMap = mapDataStructure(currentData);

  const recentPaths = new Set([...recentMap.keys()]);
  const currentPaths = new Set([...currentMap.keys()]);

  const collidingPaths = recentPaths.intersection(currentPaths);
  const uniqueRecentPaths = recentPaths.difference(currentPaths);
  const uniqueCurrentPaths = currentPaths.difference(recentPaths);

  return {

    changed: [...collidingPaths.values()]
      .reduce((result, keypath) => {

        const recentValue = recentMap.get(keypath);
        const currentValue = currentMap.get(keypath);

        if (!Object.is(recentValue, currentValue)) {

          result[keypath] = {
            recent: recentValue,
            current: currentValue,
          };
        }
        return result;

      }, {}),

    deleted: [...uniqueRecentPaths.values()]
      .reduce((result, keypath) => {

        result[keypath] = recentMap.get(keypath);
        return result;

      }, {}),

    added: [...uniqueCurrentPaths.values()]
      .reduce((result, keypath) => {

        result[keypath] = currentMap.get(keypath);
        return result;

      }, {}),
  };
}
</script>