'Compare object structure

I have an object like this:

const obj = {
        name: "one",
        created: "2022-05-05T00:00:00.0Z",
        something: {
           here: "yes"
        }
    }

I can check if it has certain attributes one by one:

assert( obj.name !== undefined && obj.name === "one")
assert( obj.something !== undefined && obj.something.here !== undefined && obj.something.here === "yes")

Is there a way to achieve the same passing another object to compare? The other object would have less values (in this example I don't care if the created attribute is present)

Something like:


const obj = {
        name: "one",
        created: "2022-05-05T00:00:00.0Z",
        something: {
           here: "yes"
        }
    }

hasAllOf(obj, {
        name: "one",
        something: {
           here: "yes"
        }
    }) // returns true

Either with lodash or some other library?



Solution 1:[1]

You can write recursive hasAllOf with a type-checking helper, is -

const is = (t, T) =>
  t?.constructor === T

const hasAllOf = (a, b) =>
    is(a, Object) && is(b, Object) || is(a, Array) && is(b, Array)
    ? Object.keys(b).every(k => hasAllOf(a[k], b[k]))
    : a === b

const obj = {
    name: "one",
    created: "2022-05-05T00:00:00.0Z",
    something: {
       here: "yes",
       and: ["this", 2, { f: "three" }]
    }
}

console.log(
    hasAllOf(obj, {
        name: "one",
        something: {
          here: "yes",
          and: ["this", 2, { f: "three" }]
        }
    })
)

Support Map and Set by using another conditional

const is = (t, T) =>
  t?.constructor === T

const hasAllOf = (a, b) => {
  switch (true) {
    case is(a, Object) && is(b, Object) || is(a, Array) && is(b, Array):
      return Object.keys(b).every(k => hasAllOf(a[k], b[k]))
    case is(a, Set) && is(b, Set):
      return [...b].every(v => a.has(v))
    case is(a, Map) && is(b, Map):
      return [...b.keys()].every(k => hasAllOf(a.get(k), b.get(k)))
    default:
      return a === b
  }
}
const obj = {
    name: new Map([[1, "one"], [2, "two"]]),
    created: "2022-05-05T00:00:00.0Z",
    something: {
       here: new Set("yes", "ok"),
       and: ["this", 2, { f: "three" }],
    }
}

console.log(
    hasAllOf(obj, {
        name: new Map([[1, "one"]]),
        something: {
          here: new Set("yes"),
          and: ["this", 2, { f: "three" }]
        }
    })
)

Solution 2:[2]

The lodash isEqual function may be what you are looking for.. https://lodash.com/docs/#isEqual

const obj = {
    name: "one",
    created: "2022-05-05T00:00:00.0Z",
    something: {
       here: "yes"
    }
}

_.isEqual(delete obj.created, delete {
    name: "one",
    created: "2022-05-05T00:00:00.0Z",
    something: {
       here: "yes"
    }}.created); //returns true

or

const obj = {
    name: "one",
    created: "2022-05-05T00:00:00.0Z",
    something: {
       here: "yes"
    }
}
delete obj.created;
_.isEqual(obj, {
    name: "one",
    something: {
       here: "yes"
    }}.created); //returns true

You could even use a different lodash function (_.cloneDeep) to copy the original so you don't mutate it.

Solution 3:[3]

    function hasAllOf(obj,data) {
      let obj_keys = Object.keys(obj)
        for (let key in data) {
        if (!(obj_keys.includes(key))) {
            return false;
        }
        if (typeof data[key] === "object" && typeof obj[key] === "object") 
        {
          let res = hasAllOf(obj[key], data[key])
          if (!res) {
             return res;
          }
          res = hasAllOf(data[key], obj[key])
          if (!res) {
             return res;
          }
        }
        else if (data[key] !== obj[key])
        {
           return false;
        }
      }
      return true;
    }
    
    
    
    const obj = 
    {
      name: "one",
      created: "2022-05-05T00:00:00.0Z",
      something: 
      {
        here: "yes",
            
    
      }
    }
        
    const data = 
    {
      name: "one",
      something:
      {
        here: "yes",
      },
    }
    
    console.log(hasAllOf(obj, data))

Solution 4:[4]

I think _.conformsTo matches closely what I'm looking for, it's a bit verbose, but much better and legible than doing all the comparisons one by one (specially in larger objects)


_.conformsTo(obj, {
        name: (n) => n === "one",
        created: (c) => _.isString(c),
        // calling _conformsTo on nested objects
        something: (s) =>  _.conformsTo(s, {
           here: (h) => h === "yes"
        })
    }) 

Update...

I can even create a is and conformsTo functions that returns a "partially applied" function and make it more legible.

Full example:


const _ = require('lodash')

const is = (expected) => (actual) => actual === expected
const conformsTo = (template) => (actual) => _.conformsTo(actual, template)

_.conformsTo(
   { // source
        name: "one",
        created: "2022-05-05T00:00:00.0Z",
        something: {
           here: "yes"
        }
    }, 
    { // "spec"
       name: is('one'),
       something: conformsTo({
         here: is("yes")
       })
  }
)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 MoRe
Solution 4