I'm used to statically typed languages and may therefore see problems that actually do not exist in Javascript. Anyway, here are the methods of type checking of user-specific objects I'm familiar with:
instanceof
Works as long as you use constructors. Since I prefer factory functions and Object.create
, instanceof
is out of question.
isPrototypeOf
Works with both constructor and factory functions, but is broken as soon as serialization comes into play (JSON or structured clone algorithm).
Works well with factory functions and serialization, but can lead to subtle errors and entails the risk of incompatibilities with external libraries/frameworks.
The thenable object (then-method) of the Promise/A+ spec is a good example. I consider that harmful.
transducer.js (and more and more other) use this pseudo symbols like @@transducer/step
to prevent naming conflicts and thus give their objects a notion of type. It's merely a variant of duck typing, but meets my requirements.
toString
I tend to use the toString method for type checking, since it is already used for native type checks via Object.prototype.toString
. Usually toString
returns the string representation of an object. I extend this behavior by letting toString return a type identifier if the method is called with an argument. The following example is highly simplified:
var proto = {}, o;
proto.toString: function (_) { return _ === undefined ? JSON.stringify(this) : "someType"};
o = Object.create(proto);
o.a = 123, o.b = "abc", o.c = true;
o.toString(); // {"a":123,"b":"abc","c":true}
o.toString(true); // someType
Since toString is mainly used for logging this should work out. However, toString
gets lost during serialization.
Are there any other methods?
Is there a best practice that can be recommended?
Update: I need type checking specifically for ad hoc polymorphism aka function/method overloading and in the context of working with web workers. Raganwald uses the terms structural and semantic typing in his blog post, which in my opinion fit very well on the subject.
Javascript is a weakly +"123" === 123 // true
and dynamically typed var obj = {}, obj = 123; // 123
language with a prototype system.
The question is how useful it is in such a language to distinguish objects at runtime by the constructors which have instantiated them. Actually constructors are merely ordinary functions in Javascript and no matter what you do, you always compare the identity of prototypes:
function Ctor() {}
var o = new Ctor();
var p = Object.create(o);
o instanceof Ctor === Ctor.prototype.isPrototypeOf(o) === true;
o.isPrototypeOf(p) === true;
So why use constructors at all? Prototypes are also just ordinary objects and thus factory functions are a logical consequence of the described inheritance and object model.
Object distinction should be based on a technique corresponding to the nature of the used language. As I said, Javascript is dynamically typed and therefore ideally suited for duck typing:
var o = {empty: function () { return this.keys().length === 0; }}
if (empty in o) {
"empty interface implemented";
}
Since keys in objects are merely string based, name collisions in connection with duck typing were inevitable. Fortunately symbols have found their way in Javascript since ES2015.
Symbols have an identity and thus can be shared without ever colliding with each other:
Symbol("foo") === Symbol("foo"); // false
Although they have identity, symbols are among the primitive types in Javascript, are immutable like strings and can be used as object keys. Duck typing with symbols evolves from a mere hash table lookup to the comparison of identities. Thus, symbols can make a reliable statement whether a certain object has implemented a certain interface:
var o = {};
var empty = Symbol("empty");
o[empty] = function () { return Object.keys(this).length === 0; }
Whenever an object has the empty
property we can be sure, it has implemented the right interface. Better yet, we can mix in various other interfaces (which use symbols as keys), look up the existence of every single of them (by their identity) and all this without name collisions or growing prototype chains.
With that things like ad hoc polymorphism (aka function overloading) can be realized with ease.
A final sentence to serialization: Serialization means to lose almost everything: Object identities, prototype chains, functions, symbols, etc.
So whenever you use a web worker or a messaging system that doesn't share memory, you have to prepare your complex objects for serialization and restore them afterwards, such as with folktale for instance.