04 January 2014

While coding in JavaScript I’ve often wished I could enforce the type of some variables. The typical case where I’ve felt the need for that is when some mathematical operation or comparision is done on a non-number, yielding wierd results but not throwing an error.

While tinkering with a hobby project (an HTML5 game) I repeatedly run into the case where a NaN was found where a number was expected. NaN is usually the result of a mathematical operation gone wrong, is of type number, and is hard to reason about.

Consider the following:

sprite.y = sprite.y + sprite.velocityY;

If sprite.velocityY is undefined or null, sprite.y will be NaN and the JavaScript interpreter won’t complain. I just can’t understand why the sprite on screen isn’t moving.

This is the type of error I encountered. At first I tried to track such bugs down by placing checks everywhere. However, it’s just after-the-facts-checking and it didn’t tell me where the value was corrupted.

The Solution

Enter Object.defineProperty. Most browsers support this feature today. When constructing an object, you can create a property with a getter and setter that acts just like a normal variable, but you have the opportunity to inspect the value. If you wanna apply this to a class, apply it to the prototype. Consider the example with the sprite:

// Class definition

function Sprite() {
  // Constructor code ...
};
  
Object.defineProperty(Sprite.prototype, "y", {
  set: function(val) {
    if (typeof val !== "number" || isNaN(val)) {
      throw "Sprite.y must be a valid number and not NaN";
    }
    this.__y = val;
  },
  get: function() {
    return this.__y;
  }

})

Sprite.prototype.y = 0;
Sprite.prototype.x = 0;

// Instantiate
  
var sprite = new Sprite();
sprite.x = "Hello, world!"; // Passed without complaints
sprite.y = sprite.somethingUndefined / 10; // Exception is thrown

…and suddenly you have an object with a strongly typed property.

Wrapping this in a function would look something like:

function typedProp(obj, key, type) {
  if (!Object.defineProperty) { // Make older browsers not crash
    return;
  }
  Object.defineProperty(obj, key, {
    set: function(val) {
      if (typeof val !== type || (type === "number" && isNaN(val))) {
        throw "Property '" + key + "' should be of type '" + type + "', value is: '" + val + "' of type '" + typeof val + "'";
      }
      this["__" + key] = val;
    },
    get: function() {
      return this["__" + key];
    }
  });
}


// ... and augment the Sprite prototype:

typedProp(Sprite.prototype, "name", "string");

sprite.name = 31337;

Since you might wanna avoid running this in a production environment, you could just have typedProp be an empty function in your release build. At least you should check for the presence of Object.defineProperty.

Some Syntactic Sugar

In my own class library, I implemented the functionality to be used as such:

CB.Class("Sprite", {
  "x:number": 0,
  "y:number": 0,
  "name:string": "defaultname",
  
  initialize: function() {
  }
  // More code goes here
});

What happens here is that I added some syntactic sugar that lets you define properties in ActionScript-like-style. Yes, you can have colons in property names. The part after the colon is stripped off and used as the type. Use my library if you want to: git@github.com:zufallsgenerator/cbclass.git, but you probably have itchy finger to build your own.

Happy coding!

Edit: strictly typed -> strongly typed Edit: fixed typo _Edit: fixed http -> https