JavaScript: Deep check objects have same keys
Asked Answered
S

4

8

Question is similar to: How can I check that two objects have the same set of property names? but only one difference

I want to check:

var objOne = {"a":"one","b":"two","c":{"f":"three_one"}};
var objTwo = {"a":"four","b":"five","c":{"f":"six_one"}};

have the same set of keys in all level?

For example deepCheckObjKeys(objOne, objTwo) would return true where deepCheckObjKeys(objOne, objThree) return false, if:

var objThree = {"a":"four","b":"five","c":{"g":"six_one"}};

Since objThree.a.c.f is undefined in objThree.

A function like this:

'use strict';

function objectsHaveSameKeys() {
   for (var _len = arguments.length, objects = Array(_len), _key = 0; _key < _len; _key++) {
      objects[_key] = arguments[_key];
   }

   var allKeys = objects.reduce(function (keys, object) {
      return keys.concat(Object.keys(object));
   }, []);
   var union = new Set(allKeys);
   return objects.every(function (object) {
      return union.size === Object.keys(object).length;
   });
}

only checks the first level.

PS: objectsHaveSameKeys() ES6 equivalent:

function objectsHaveSameKeys(...objects):boolean {
   const allKeys = objects.reduce((keys, object) => keys.concat(Object.keys(object)), []);
   const union = new Set(allKeys);
   return objects.every(object => union.size === Object.keys(object).length);
}
Speller answered 23/1, 2017 at 8:51 Comment(0)
U
11

I'd do a recursive check if a property's value is an object.

There's an interesting wrinkle here; actually, there are (at least) two:

  • What if one of the "objects" is null and the other has no properties? true or false?
  • What if one of the objects has {a: null} and the other has {a: 17}? true or false?
  • What if one of the objects has {a: null} and the other has {a: {}}? true or false?

For the purposes of this example, I've treated null like an object with no properties, but it's very much dependent on your use case. I can think of at least two other ways to go (null doesn't match anything but null, or null doesn't match anything but a non-object, even if the object has no own properties) and there are probably others.

See comments:

const deepSameKeys = (o1, o2) => {
    // Both nulls = same
    if (o1 === null && o2 === null) {
        return true;
    }

    // Get the keys of each object
    const o1keys = o1 === null ? new Set() : new Set(Object.keys(o1));
    const o2keys = o2 === null ? new Set() : new Set(Object.keys(o2));
    if (o1keys.size !== o2keys.size) {
        // Different number of own properties = not the same
        return false;
    }

    // Look for differences, recursing as necessary
    for (const key of o1keys) {
        if (!o2keys.has(key)) {
            // Different keys
            return false;
        }
        
        // Get the values and their types
        const v1 = o1[key];
        const v2 = o2[key];
        const t1 = typeof v1;
        const t2 = typeof v2;
        if (t1 === "object") {
            if (t2 === "object" && !deepSameKeys(v1, v2)) {
                return false;
            }
        } else if (t2 === "object") {
            // We know `v1` isn't an object
            return false;
        }
    }

    // No differences found
    return true;
};

// Checking your example
const objOne   = {"a": "one",  "b": "two",  "c": {"f": "three_one"}};
const objTwo   = {"a": "four", "b": "five", "c": {"f": "six_one"}};
const objThree = {"a": "four", "b": "five", "c": {"g": "six_one"}};

console.log("objOne vs. objTwo:         ", deepSameKeys(objOne, objTwo));        // true
console.log("objTwo vs. objThree:       ", deepSameKeys(objTwo, objThree));      // false

// `null` checks
console.log("{a: null} vs. {a: 17}      ", deepSameKeys({a: null}, {a: 17}));    // true
console.log("{a: null} vs. {a: {}}      ", deepSameKeys({a: null}, {a: {}}));    // true -- depending on your use case, you may want this to be false
console.log("{a: null} vs. {a: {x:1}}   ", deepSameKeys({a: null}, {a: {x:1}})); // false

// Differing value type check
console.log("{a: 1} vs. {a: '1'}}       ", deepSameKeys({a: 1}, {a: '1'}));      // true
Underexpose answered 23/1, 2017 at 9:0 Comment(12)
Nice. Could I ask why you are declaring inner variables as const, and not as var ?Jazminejazz
@ThirueswaranRajagopalan: In ES2015 and above, I default to const. I only use let if I need to change the value of the variable at some point, and I didn't need to change those. I don't use var at all. (Er, except I see I did when copying your objOne and such above. :-) )Underexpose
@ThirueswaranRajagopalan: Well, me and many others. But yes, it's pretty much a style thing, although it may be helpful to the compiler to know that that variable won't change without having to do the necessary static analysis to prove it...Underexpose
Note that if t1 is "object" and either v1 or v2 is null, the recursive call will throw an exception on the Object.keys(null). You need to check for null either at the top of the function or before recursing.Recessive
Having implemented this myself now, I realized that arrays need to be handled specially as well. I think in most cases where you're checking the shape of an object, you'd want to treat { foo: [1, 2] } and { foo: [] } as having the same keys, but the code above will return false, since the arrays have different keys (which is true, but not really relevant).Recessive
@Recessive - Totally depends on your use case, but sure, sometimes.Underexpose
Both deepSameKeys({ a: null }, { a: 17 }) and deepSameKeys({ a: '1' }, { a: 1 }) return falsePooch
@Pooch - Great catch, thanks! I've updated the answer.Underexpose
Is if (t2 === "object") { if (t1 === "object" && !deepSameKeys(v1, v2)) { return false; } } reachable? under what condition?Cinematograph
@WilliamNeely - The return false couldn't, no, good catch, I've updated it.Underexpose
You really went the extra mile with this one T.J., I appreciate it! Great example BTW. The 3 objects you choose to execute it against really made this answer great.Indiction
You know @T.J.Crowder I think this is one of the most beautiful blog reads I have had the pleasure of reading. thenewtoys.dev/blog/2020/07/20/kindnessIndiction
G
1

I guess you're looking for a deep-check version of the function provided [here] :)(How can I check that two objects have the same set of property names?).

Below is my attempt. Please note:

  • Solution does not check for null and is not bullet proof
  • I haven't performance tested it. Maybe the OP or anyone else can do that and share for the community.
  • I'm not a JS expert :).
function objectsHaveSameKeys(...objects) {
  const allKeys = objects.reduce((keys, object) => keys.concat(Object.keys(object)), [])
  const union = new Set(allKeys)
  if (union.size === 0) return true
  if (!objects.every((object) => union.size === Object.keys(object).length)) return false

  for (let key of union.keys()) {
    let res = objects.map((o) => (typeof o[key] === 'object' ? o[key] : {}))
    if (!objectsHaveSameKeys(...res)) return false
  }
  return true
}
Gilbert answered 30/7, 2021 at 9:9 Comment(0)
R
0

You can create recursive function that will return all keys and check if they are equal with every().

var objOne = {"a":"one","b":"two","c":{"f":"three_one"}};
var objTwo = {"a":"four","b":"five","c":{"f":"six_one"}};

function checkKeys(obj1, obj2) {

  function inner(obj) {
    var result = []

    function rec(obj, c) {
      Object.keys(obj).forEach(function(e) {
        if (typeof obj[e] == 'object') rec(obj[e], c + e)
        result.push(c + e)
      })
    }
    rec(obj, '')
    return result
  }

  var keys1 = inner(obj1), keys2 = inner(obj2)
  return keys1.every(e => keys2.includes(e) && keys1.length == keys2.length)
}

console.log(checkKeys(objOne, objTwo))
Rubato answered 23/1, 2017 at 9:12 Comment(1)
{a: true, bc:true, b:true} and {a: true, b:{c: true}} incorrectly returns trueMerrilee
F
0

This function checks only keys, and will check deeply until it finds something that does not match.

It's written in TypeScript:

function checkSameKeys(obj1: { [key: string]: any }, obj2: { [key: string]: any }) {
if (obj1 === null || !obj2 === null) {
    return false
}

const obj1Keys = Object.keys(obj1)
const obj2Keys = Object.keys(obj2)

if (obj1Keys.length !== obj2Keys.length) {
    return false
}

for (const key of obj1Keys) {
    if (obj1[key] !== null && typeof obj1[key] === 'object') {
        if (!checkSameKeys(obj1[key], obj2[key])) {
            return false
        }
    } else if (!obj2Keys.includes(key)) {
        return false
    }
}

return true

}

Feme answered 2/6, 2023 at 13:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.