5

I have found this (personally) convenient answer that fits my needs: https://stackoverflow.com/a/6713782/2678218

But since I am using TypeScript, I can have something like this with Generics:

private equals<T>(x: T, y: T) {
    if (x === y) {
        return true; // if both x and y are null or undefined and exactly the same
    } else if (!(x instanceof Object) || !(y instanceof Object)) {
        return false; // if they are not strictly equal, they both need to be Objects
    } else if (x.constructor !== y.constructor) {
        // they must have the exact same prototype chain, the closest we can do is
        // test their constructor.
        return false;
    } else {
        for (const p in x) {
            if (!x.hasOwnProperty(p)) {
                continue; // other properties were tested using x.constructor === y.constructor
            }
            if (!y.hasOwnProperty(p)) {
                return false; // allows to compare x[ p ] and y[ p ] when set to undefined
            }
            if (x[p] === y[p]) {
                continue; // if they have the same strict value or identity then they are equal
            }
            if (typeof (x[p]) !== 'object') {
                return false; // Numbers, Strings, Functions, Booleans must be strictly equal
            }
            if (!this.equals(x[p], y[p])) {
                return false;
            }
        }
        for (const p in y) {
            if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) {
                return false;
            }
        }
        return true;
    }
}

I'm sure that since we're using <T> here, we can refactor the code. One thing's for sure is by removing some if statements that are not needed anymore. But i am not confident of which to take away and also not sure if there will be a more optimal code for this. So I'll leave the question here and have everyone vote for the best answer.

In this context, what I actually mean by equality of two objects is when two objects of the same type have equal values of every property.

Alex Pappas
  • 2,377
  • 3
  • 24
  • 48
  • 1
    generics is only for compile time, you have to do all the checks anyway since type "any" can be passed in. – Lostfields Oct 12 '17 at 10:51
  • "*same prototype chain, the closest we can do is test their constructor.*" - oh my, you should definitely update that code with modern `Object.getPrototypeOf` – Bergi Oct 12 '17 at 14:02

1 Answers1

4

@Lostfields mentions that someone could pass any for the type T, but that's not a big concern, since using any is telling the compiler not to type check anything. If this leads to undesirable behavior at runtime, I'd place the responsibility for dealing with this on the code that passed in any, instead of the code inside equals(). One use of the type system is indeed to eliminate some unnecessary runtime checks, with the caveat that you still need to do sanitize any data being passed in from untrusted sources. Are you building a library that will be used by developers who might not even be using TypeScript? Then do not relax any runtime checks. Are you building code to be used internally or by other TypeScript developers who depend on your typings? Then by all means eliminate unnecessary checks.


That being said, I don't you can remove many of the checks in that implementation. Each of the conditionals checked might be true or false at runtime, even knowing that TypeScript has decided that x and y are of the same type. (In what follows I will be treating equals() as a standalone function instead of a method. Add this or whatever the object name is as you see fit)

Let's examine each one:

  • (x === y): True for equals(x,x), false for equals(x, Object.assign({},x)). This one has to stay.

  • ((!(x instanceof Object) || !(y instanceof Object)): This one you might decide to replace with just (!(x instanceof Object)), since in practice a type in TypeScript is either an Object or it is not, and so x instanceof Object should be the same as y instanceof Object. Still, someone might do equals(0, new Number(0)) which passes the type check in TypeScript. It's up to you if you care about guarding against that.

  • (x.constructor !== y.constructor): False for two structurally identical classes, such as class A{}; class B{}; equals(new A(), new B()). If you're not worried about structurally identical but distinct classes, you can eliminate this check.

  • (!x.hasOwnProperty(p)): This check has nothing to do with TypeScript; it has to stay.

For the next case, consider

interface Foo { foo?: string, bar: number, baz?: boolean };
const x: Foo = { foo: 'hello', bar: 12 };
const y: Foo = { bar: 12, baz: false };
equals(x, y);
  • (!y.hasOwnProperty(p)) and (y.hasOwnProperty(p) && !x.hasOwnProperty(p)): These could be true or false for instances of Foo, or any type with optional properties. Or any subtype of a type without optional properties, since extra properties are allowed in TypeScript.

  • (x[p] === y[p]), (typeof (x[p]) !== 'object'), (!equals(x[p], y[p])): These can be true or false for the same reasons as above, which can be seen by passing in a type with a single property of the types mentioned above. That is, if equals(x,y) needs a runtime check, then equals({foo: x},{foo: y}) will need the same runtime check.


So, it's up to you. Feel free to leave the implementation alone. Extra runtime checks don't hurt anything, after all. Feel free to remove some checks if you don't think you'll need them; again, you're the only one who knows how crazy the users of equals() will be. For example, what would you do about this:

interface Ouroboros {
    prop: Ouroboros;
}
let x = {} as Ouroboros;
x.prop = x;

let y = {} as Ouroboros;
y.prop = y;

console.log(equals(x,y))

Do you care about circular references? If not, don't worry. If so, then you need to harden your equality check to deal with it.


Hope that helps; good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    Awesome, Thank you so much. Learned a lot from this. And yes, I'm trying my best to make the function idiot proof. lol. So regarding the circular reference example, Can we add a checking for it? – Alex Pappas Oct 13 '17 at 02:37
  • Yes, you can keep a cache of references and bail out if you encounter the same reference twice. Whether you bail out with `true`, `false`, or something context-dependent is up to you and what you think about equality. – jcalz Oct 13 '17 at 12:42