Fixing the JavaScript typeof operator

Posted: August 8, 2011 | Author: | Filed under: JavaScript

Working with JavaScript’s typeof operator is a bit like operating a clapped-out old car (or an early model Dell Inspiron). It gets the job done (mostly) and you learn to work around the quirks – but you probably aspire to something better.

In this article I’ll give a brief overview of typeof before introducing a tiny new function which is a fully-loaded, more reliable alternative that works directly with the language internals.

The typeOf Operator

How is it used?

Since typeof is a unary operator, the operand follows the operator. No additional punctuation is required.

typeof 2 //"number"
typeof "belladonna" //"string"

But it works when I call it as a function?

The typeof operator is not a function. You can surround the operand with parentheses so that the expression looks like a function call, but the parentheses will simply act as a grouping operator (second only to the comma operator in the obscurity pecking order!). In fact you can decorate the operand with all manner of punctuation without derailing the operator.

typeof (2) //"number"
typeof(2) //"number"
typeof ("a", 3) //"number"
typeof (1 + 1) //"number"

What does it return?

The returned value is a somewhat arbitrary representation of the operand’s type. The table below (based on the one in the ES5 spec) provides a summary:

Type of val Result
Undefined undefined
Null object
Boolean boolean
Number number
String string
Object (native and not callable) object
Object (native or host and
callable)
function
Object (host and not
callable)
Implementation-defined

What’s wrong with typeof?

The most glaring issue is that typeof null returns “object”. It’s simply a mistake. There’s talk of fixing it in the next version of the ECMAScript specification, although this would undoubtedly introduce backwards compatibility issues.

1 var a;
2 typeof a; //"undefined"
3 typeof b; //"undefined"
4 alert(a); //undefined
5 alert(b); //ReferenceError

Other than that, typeof is just not very discriminating. When typeof is applied to any object type other than Function, it returns “object”. It does not distinguish between generic objects and the other built-in types (Array, Arguments, Date, JSON, RegExp, Math, Error, and the primitive wrapper objects Number, Boolean and String).

Oh and you’ll hear folks complaining about this…

typeof NaN //"number"

…but that’s not the fault of the typeof operator since the standard clearly states that NaN is indeed a number.

A Better Way?

[[Class]]

Every JavaScript object has an internal property known as [[Class]] (The ES5 spec uses the double square bracket notation to represent internal properties, i.e. abstract properties used to specify the behavior of JavaScript engines). According to ES5, [[Class]] is “a String value indicating a specification defined classification of objects”. To you and me, that means each built-in object type has a unique non-editable, standards-enforced value for its [[Class]] property. This could be really useful if only we could get at the [[Class]] property…

Object.prototype.toString

…and it turns out we can. Take a look at the ES 5 specification for Object.prototype.toString…

  1. Let O be the result of calling ToObject passing the this value as the argument.
  2. Let class be the value of the [[Class]] internal property of O.
  3. Return the String value that is the result of concatenating the three Strings "[object ", class, and "]".

In short, the default toString function of Object returns a string with the following format…

[object [[Class]]]

…where [[Class]] is the class property of the object.

Unfortunately, the specialized built-in objects mostly overwrite Object.prototype.toString with toString methods of their own…

1 [1,2,3].toString(); //"1, 2, 3"
2
3 (new Date).toString(); //"Sat Aug 06 2011 16:29:13 GMT-0700 (PDT)"
4
5 /a-z/.toString(); //"/a-z/"

…fortunately we can use the call function to force the generic toString function upon them…

1 Object.prototype.toString.call([1,2,3]); //"[object Array]"
2
3 Object.prototype.toString.call(new Date); //"[object Date]"
4
5 Object.prototype.toString.call(/a-z/); //"[object RegExp]"

Introducing the toType function

We can take this technique, add a drop of regEx, and create a tiny function – a new and improved version of the typeOf operator…

1 var toType = function(obj) {
2   return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
3 }

(since an new, generic object will always use the toString function defined by Object.prototype we can safely use ({}).toString as an abbreviation for Object.prototype.toString)

Let’s try it out…

01 toType({a: 4}); //"object"
02 toType([1, 2, 3]); //"array"
03 (function() {console.log(toType(arguments))})(); //arguments
04 toType(new ReferenceError); //"error"
05 toType(new Date); //"date"
06 toType(/a-z/); //"regexp"
07 toType(Math); //"math"
08 toType(JSON); //"json"
09 toType(new Number(4)); //"number"
10 toType(new String("abc")); //"string"
11 toType(new Boolean(true)); //"boolean"

..and now we’ll run the same tests with the typeof operator (and try not to gloat) …

01 typeof {a: 4}; //"object"
02 typeof [1, 2, 3]; //"object"
03 (function() {console.log(typeof arguments)})(); //object
04 typeof new ReferenceError; //"object"
05 typeof new Date; //"object"
06 typeof /a-z/; //"object"
07 typeof Math; //"object"
08 typeof JSON; //"object"
09 typeof new Number(4); //"object"
10 typeof new String("abc"); //"object"
11 typeof new Boolean(true); //"object"

Compare to duck-typing

Duck-typing checks the characteristics of an object against a list of known attributes for a given type (walks like a duck, talks like a duck…). Because of the limited usefulness of the typeof operator, duck-typing is popular in JavaScript. Its also error-prone. For example the arguments object of a Function has a length property and numerically indexed elements, but it is still not an Array.

Using toType is a reliable and easy alternative to duck-typing. Reliable because it talks directly to the internal property of the object, which is set by the browser engine and is not editable; easy because its a three-word check.

Here’s an illustrative example – a snippet which defines a non-compliant JSON object. The jsonParseIt function accepts a function as its argument, which it can use to test the veracity of the JSON object before using it to parse a JSON string….

1 window.JSON = {parse: function() {alert("I'm not really JSON - fail!")}};
2
3 function jsonParseIt(jsonTest) {
4   if (jsonTest()) {
5     return JSON.parse('{"a":2}');
6   } else {
7     alert("non-compliant JSON object detected!");
8   }
9 }

Let’s run it, first with duck-typing…

jsonParseIt(function() {return JSON && (typeof JSON.parse == "function")})
//"I'm not really JSON - fail!"

…whoops!…and now with the toType test…

jsonParseIt(function() {return toType(JSON) == "json"});
//"non-compliant JSON object detected!"

Could toType reliably protect against the malevolent swapping of built-in JavaScript objects with impostors? Probably not, since the perpetrator could presumably also swap the toType function. A more secure test might call ({}).toString directly…

function() { return ({}).toString.call(JSON).indexOf("json") > -1 }

..though even this would fail if Object.prototype.toString was itself maliciously re-written. Still each additional defense helps.

Compare to instanceof

The instanceof operator tests the prototype chain of the first operand for the presence of the prototype property of the second operand (the second operand is expected to be a constructor, and a TypeError will be thrown if it is not a Function):

1 new Date instanceof Date; //true
2
3 [1,2,3] instanceof Array; //true
4
5 function CustomType() {};
6 new CustomType instanceof CustomType; //true

On the face of it this seems to hold promise of a nice type-checker for built-ins, however there are at least two snags with this approach:

1. Several built in objects (Math, JSON and arguments) do not have associated constructor objects – so they cannot be type-checked with the instanceof operator.

Math instanceof Math //TypeError

2. As @kangax and others have pointed out, a window can comprise multiple frames, which means multiple global contexts and therefore multiple constructors for each type. In such an environment, a given object type is not guaranteed to be an instanceof of a given constructor….

1 var iFrame = document.createElement('IFRAME');
2 document.body.appendChild(iFrame);
3
4 var IFrameArray = window.frames[1].Array;
5 var array = new IFrameArray();
6
7 array instanceof Array; //false
8 array instanceof IFrameArray; //true;

Type-checking host objects

Host objects are browser-created objects that are not specified by the ES5 standard. All DOM elements and global functions are host objects. ES5 declines to specify a return value for typeof when applied to host objects, neither does it suggest a value for the [[Class]] property of host objects. The upshot is that cross-browser type-checking of host objects is generally not reliable:

01 toType(window);
02 //"global" (Chrome) "domwindow" (Safari) "window" (FF/IE9) "object" (IE7/IE8)
03
04 toType(document);
05 //"htmldocument" (Chrome/FF/Safari) "document" (IE9) "object" (IE7/IE8)
06
07 toType(document.createElement('a'));
08 //"htmlanchorelement" (Chrome/FF/Safari/IE) "object" (IE7/IE8)
09
10 toType(alert);
11 //"function" (Chrome/FF/Safari/IE9) "object" (IE7/IE8)

The most dependable cross-browser test for an element might be to check for the existence of a nodeType property…

function isElement(obj) {
  return obj.nodeType;
}

…but that’s duck-typing so there are no guarantees

Where should a toType function live?

For brevity, my examples define toType as a global function. Extending Object.prototype will get you thrown to the dragons – my preference would be to extend Object directly, which mirrors the convention established by ES5 (and prototype.js before that).

1 Object.toType = function(obj) {
2   return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase();
3 }

Alternatively you might choose to add the toType function to a namespace of your own, such as util.

We could get a little cleverer (inspired by Chrome’s use of “global” for window.[[Class]]). By wrapping the function in a global module we can identify the global object too:

1 Object.toType = (function toType(global) {
2   return function(obj) {
3     if (obj === global) {
4       return "global";
5     }
6     return ({}).toString.call(obj).match(/\s([a-z|A-Z]+)/)[1].toLowerCase();
7   }
8 })(this)

Let’s try it out…

1 Object.toType(window); //"global" (all browsers)
2 Object.toType([1,2,3]); //"array" (all browsers)
3 Object.toType(/a-z/); //"regexp" (all browsers)
4 Object.toType(JSON); //"json" (all browsers)
5 //etc..

What toType does not do

The toType function cannot protect unknown types from throwing ReferenceErrors…

Object.toType(fff); //ReferenceError

More precisely it is the call to toType that throws the error, not the function itself. The only guard against that (as with calls to any function) is to practice good code hygiene…

window.fff && Object.toType(fff);

Wrap Up

OK I’ve babbled on for far longer than I intended to – so congratulations if you made it to here, I hope you found it useful. I covered a lot of ground and probably made some mistakes – please feel free to let me know about them. Also I’d love to hear about other people’s adventures in type-checking.

Further Reading

Juriy Zaytsev (“kangax”):
‘instanceof’ considered harmful (or how to write a robust ‘isArray’)

ECMA-262 5th Edition:
The typeof operator
Object Internal Properties and Methods (for more about [[Class]])
Object.prototype.toString
The instanceof operator



Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s