Thomas
Thomas

Reputation: 6196

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.

Upvotes: 1

Views: 483

Answers (3)

Michael Hobbs
Michael Hobbs

Reputation: 1693

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.

Upvotes: 1

cWerning
cWerning

Reputation: 633

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
}

Upvotes: 1

IceMetalPunk
IceMetalPunk

Reputation: 5566

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.

Upvotes: 4

Related Questions