JavaScript: Deep check objects have same keys

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 = yes
    if (o1 === null && o2 === null) {
        return true;
    }
    // Get the keys of each object
    const o1keys = o1 === null ? [] : Object.keys(o1);
    const o2keys = o2 === null ? [] : Object.keys(o2);
    if (o1keys.length !== o2keys.length) {
        // Different number of own properties = not the same
        return false;
    }
    
    // At this point, one of two things is true:
    // A) `o1` and `o2` are both `!null`, or
    // B) One of them is `null` and the other has own "own" properties
    // The logic below relies on the fact we only try to use `o1` or
    // `o2` if there's at least one entry in `o1keys`, which we won't
    // given the guarantee above.

    // Handy utility function
    const hasOwn = Object.prototype.hasOwnProperty;

    // Check that the keys match and recurse into nested objects as necessary
    return o1keys.every(key => {
        if (!hasOwn.call(o2, 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;
            }
        }
        if (t2 === "object") {
            if (t1 === "object" && !deepSameKeys(v1, v2)) {
                return false;
            }
        }
        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