简体   繁体   中英

structural type checking in javascript

I'm wondering if there is a common way, perhaps a library, to check the structure of objects (like duck typing).

This can be useful for both runtime type-checking, and for writing unit tests.

I think I'm looking for something similar to typescript "interfaces', bu typescript only does static checking.

No simple way, but how about a utility function?:

function isOfType(obj, model) {
  for (let prop in model) {
    if (!(prop in obj) || typeof obj[prop] !== typeof model[prop] || Array.isArray(model[prop]) !== Array.isArray(obj[prop])) {
      return false;
    }
    if (typeof model[prop] === 'object' && !Array.isArray(model[prop])) {
      if (!isOfType(obj[prop], model[prop])) {
        return false;
      }
    }
  }
  return true;
}

So basically, you can compare any object to a model with this. It will make sure the object has all the properties the model has, of the same types, and recursively apply that to nested objects.

Even with Typescript interfaces, there isn't an easy comparison to make sure the structure of a object matches type. For sanity checking, I've used a conditional operator to check all needed properties of the object:

yourObject = {
  name: 'cw';
  work: {
    employed: true;
    company: 'stackoverflow'
  }
}
if (yourObject &&
  yourObject.hasOwnProperty('name') &&
  yourObject.hasOwnProperty('work') &&
  yourObject.work.hasOwnProperty('employed') &&
  yourObject.work.hasOwnProperty('company')
) {
  //structure === good
}

UPDATE: If you want duck typing as defined as https://en.wikipedia.org/wiki/Duck_typing then check-types.js https://gitlab.com/philbooth/check-types.js has you covered. See check.like(...) Additionally, based on the wiki article IceMetalPunk's solution also holds up.

Additionally, I've created a codesandbox: https://codesandbox.io/embed/optimistic-tu-d8hul?expanddevtools=1&fontsize=14&hidenavigation=1

Original answer, not exactly correct: "Going to have to give this a soft "no". There is no way to 100% know that Object A is structurally an unmodified object of class X. If you are less strict then the answer would be a soft "yes". If you are looking to compare A to B then you can compare props. Again, though you could have a case where both A and B come from the same parent class X and have not been mutated by any outside force other than calling the objects own function."

Borrowing a function from MDN to get started.

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects
function listAllProperties(o) {
    var objectToInspect;
    var result = [];

    for(objectToInspect = o; objectToInspect !== null; objectToInspect = Object.getPrototypeOf(objectToInspect)) {
        result = result.concat(Object.getOwnPropertyNames(objectToInspect));
    }

    return result;
}

This function will check if our Object A is effectively the same as our base object created from class X.

function isPureObject(baseObj, objToCheck) {
    // instanceof to make sure we don't have a object that has the same starting definition but is actually different class
    if (!(objToCheck instanceof baseObj.constructor)) return false
    let baseProps = listAllProperties(baseObj)
    return listAllProperties(objToCheck).every(prop => baseProps.indexOf(prop) > -1)
}

Now let's create a couple of test classes.

class Test {
    constructor(b) { this.b = b }
    a() { this.d = 18}
    c = 5
}

// this is effective the same as Test but is a different class
class LikeTest {
    constructor(b) { this.b = b }
    a() { this.d = 18 }
    c = 5
}

Create some new test objects

let a = new Test(3)
let b = new Test(42)
let c = new Test(42)
let likeTest = new LikeTest(3)
c.z = 10

let base = new Test(0)

For our first set of test, we will show that our function "isPureObject" can correctly test that A is an object of class X and has not been mutated outside the starting template. I've also included IceMetalPunk's function isOfType and check.like for comparison.

Test basic cases where the test object "is" a duck that has not been mutated.

console.log(`Test basic cases where the test object "is" a duck that has not been mutated.`);
console.log(`------------------------------------------------------------`);
console.log(`expect true - isPureObject(base, a) = ${isPureObject(base, a)}`);
console.log(`expect true - isOfType(a, base)     = ${isOfType(a, base)}`);
console.log(`expect true - check.like(a, base)   = ${check.like(a, base)}`);
console.log(`expect true - isPureObject(base, b) = ${isPureObject(base, b)}`);
console.log(`expect true - isOfType(b, base)     = ${isOfType(b, base)}`);
console.log(`expect true - check.like(b, base)   = ${check.like(b, base)}`);

Test cases where the test object "is" a mutated duck.

console.log(`\n\nTest cases where the test object "is" a mutated duck.`);
console.log(`------------------------------------------------------------`);
console.log(`expect false - isPureObject(base, c) = ${isPureObject(base, c)}`);
console.log(`expect true  - isOfType(c, base)     = ${isOfType(c, base)}`);
console.log(`expect true  - check.like(c, base)   = ${check.like(c, base)}`);

Test cases where the test object "is like" a duck but not a duck.

console.log(`\n\nTest cases where the test object "is like" a duck but not a duck.`);
console.log(`------------------------------------------------------------`);
console.log(`expect false - isPureObject(base, likeTest) = ${isPureObject(base,likeTest)}`);
console.log(`expect true  - isOfType(likeTest, base)     = ${isOfType(likeTest, base)}`);
console.log(`expect true  - check.like(likeTest, base)   = ${check.like(likeTest, base)}`);

And lastly, we show why this is such a hard problem by having the object under test mutate in an intended way and make isPureObject function fail.

a.a();
console.log('\n\nCalled a.a() which sets this.d to a value that was not previously defined.')
console.log(`------------------------------------------------------------`);
console.log(`expect true - isPureObject(base, a) after calling a.a() = ${isPureObject(base, a)}`)
console.log(`expect true - isOfType(a, base) after calling a.a()     = ${isOfType(a, base)}`)
console.log(`expect true - check.like(a, base) after calling a.a()   = ${check.like(a, base)}`)

Original answer: "Again, I can't give this a hard no or a hard yes as I suspect there are ways this could be done using an object.constructor.toSting() to compare against the objects current state but even that may not be enough. I also know that React.js does something along these lines but they may be doing it against very specific objects/class whereas I assume you are looking for a broad general uses case."

Updated: It really depends on what you want to do. If you are looking for duck typing then there are many solutions. A few have been covered here. If you are looking for a structural/unmutated object then isPureObject will handle that. However, it will fall short on objects that can self-mutate.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM