0

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?

OscarRyz
  • 196,001
  • 113
  • 385
  • 569
  • So object A can have a subset of object B properties? There is a similar SO question: [How to determine equality for two JavaScript objects?](https://stackoverflow.com/questions/201183/how-to-determine-equality-for-two-javascript-objects), but seems like there might be a better way since it was answered 13 years ago. – Yogi May 05 '22 at 18:58
  • Do arrays need to be considered? – Ben Stephens May 05 '22 at 20:04
  • 1
    @Yogi more like using B as a template to verify A. I think I found it with `_.conformsTo` see my answer below. – OscarRyz May 05 '22 at 20:41
  • @BenStephens Yes – OscarRyz May 05 '22 at 20:41

4 Answers4

2

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" }]
        }
    })
)
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • If arrays are required then my guess would be that the position in the array shouldn't matter; this will fail if you have something in the requirement array at a different position to where it is found in the array that's being checked. – Ben Stephens May 05 '22 at 20:37
  • sure, maybe, but i think array positions are important. if you wanted to match an unordered type, you could add another conditional check for `Set`. – Mulan May 05 '22 at 20:38
  • @BenStephens i updated the answer to show such a technique – Mulan May 05 '22 at 21:08
1

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.

Bryce
  • 84
  • 7
1

    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))
MoRe
  • 2,296
  • 2
  • 3
  • 23
1

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")
       })
  }
)
OscarRyz
  • 196,001
  • 113
  • 385
  • 569