One of the frequently expressed frustrations with JavaScript, especially from users coming from more static languages, is the lack of typing capabilities in JavaScript. Typing provides the ability to define and enforce contracts between interfaces such that interactions can be validated before violated assumptions result in later difficult-to-find bugs. On the other hand, many users that have grown fond of JavaScript have come to enjoy the fast and free style, and the minimalism of simply defining behavior without needing to make extra type definitions. However, these two coding preferences do not need to be mutually exclusive.

The abandoned ES4 effort made a valiant attempt to add typing (without losing dynamism) via gradual typing; however this still demanded the inclusion of intrusive typing annotations—thus adding type constraints to code polluted the otherwise simple direct JavaScript programming style necessitating more complex, difficult-to-follow code. Other efforts include libraries to define types on functions without extending syntax.

It is now possible to unobtrusively define type constraints on properties and methods in JavaScript with portable JSON schema based definitions called JSON schema interfaces (JSI). JSI is a typing system for JavaScript that does not force any new syntax on JavaScript. Schema structures can be used to define the type constraints in a way that works with existing JavaScript classes and objects.

The new typing module can be used both as a standalone bundle (with the JavaScript JSON schema library) which can be downloaded now. It also can be used as a Dojo module, available as dojox.lang.typed. Adding type constraints to JavaScript classes is very simple; the type constraints follow the JSON schema structure, where the class acts as the root of the schema. One can define property constraints in the properties property. The JSI extension to standard JSON schemas also allows method signatures to be defined in the methods property, with the ability to define the types for parameters and return values.

Using Typed Classes

To demonstrate, first we will create a normal JavaScript class using the standard prototype pattern:

Balloon = function(diameter){
  this.diameter = diameter;
};
Balloon.prototype = {
  getVolume: function(){
     return Math.pow(this.diameter, 3) * Math.PI / 6;
  },
  setVolume: function(volume){
     this.diameter = Math.pow(volume * 6 / Math.PI, 1/3);
  }
};

This class should work fine on its own, but if we want to define constraints to ensure that its interface is properly exposed and used as expected, we can declare the class as typed and add type constraints. Using the standalone library, we first define the class as type-able:

Balloon = typed(Balloon); 

And now we can define constraints:

Balloon.properties = {
  diameter:{
     type:"number",
     minimum: 0,
     description:"This is the diameter of the balloon"
  }
}; 

Now we can try out our constraints:

// this will work normally
goodBalloon = new Balloon(2);
// this will throw a TypeError because the diameter must be a number
goodBalloon.diameter = "not a number";
// this will throw a TypeError as it results in an invalid value for the diameter
badBalloon = new Balloon("not a number");
// this will throw a TypeError because the diameter must be at least zero.
goodBalloon.diameter = -10;

This is effectively JSON schema applied to JavaScript objects with live constraints. Rather than just validating the state of the object at some point in time, properties are kept valid throughout the life of the object. Notice also that the property definition we created only constrains a single property; any other properties can be added or removed from the object freely. Schemas are additive in their constraints; an empty schema does not enforce any constraints on an object, and it can take any form.

However, JSI takes us beyond just constraining properties. We can also constrain the calls to methods. To do this, we can create method definitions in a similar way that we create property definitions in a schema.

A method definition has two important attributes: parameters and returns. The parameters attribute takes an array which defines the type for each parameter value to be passed to the method by position. The returns attribute takes a schema/type definition to constrain what values can be returned by the method. We could define types on the methods that the class implements (those defined on the prototype):

Balloon.methods = {
  setVolume:{
    parameters:[
      {
         type:"number",
         minimum: 0,
         description:"The new volume for the balloon."
       }
     ],
     description: "Set the volume (changes the diameter property)"
  },
  getVolume:{
    parameters:[],
    returns:{
       type:"number",
       minimum: 0,
       description:"The volume of the balloon."
     }  
  }
}; 

Now we can try out the method definitions:

// create a new balloon:
goodBalloon = new Balloon(2);
// will work properly
goodBalloon.setVolume(10);
// this will throw a TypeError
goodBalloon.setVolume("big");

We can also define class inheritance with our schemas, as well as utilize composition for property and method definitions. Suppose we had defined a subclass of Balloon called ColoredBalloon:

function ColoredBalloon(color){
  this.color = color;
}
ColoredBalloon.prototype = new Balloon(0);
ColoredBalloon = typed(ColoredBalloon);

We can define the inheritance structure in our schema when we define the schema for the subclass:

ColoredBalloon["extends"] = Balloon;

We can use composition for our property definitions. If we have another class called Color:

function Color(red, green, blue){
   ...
}

We could define a color property for our ColoredBalloon class that takes Color instances:

ColoredBalloon.properties = {
  color: Color
};

Now we can try our color property:

balloon = new ColoredBalloon(new Color(33,44,55));
// assign a color, this should work
balloon.color = new Color(255, 0, 0);
// this will fail, since the value is not an instance of Color (instanceof check is used)
balloon.color = "red";
// this will fail, since we inherit the property definitions from Balloon
balloon.diameter = "small";

We can also use existing classes in method definitions as well. For example, a method definition for a java-bean style setter for color would look like:

ColoredBalloon.methods = {
  setColor: {
    parameters:[Color]
  }
};

And all the method definitions are inherited from Balloon (as well as the method implementations through standard JavaScript prototype inheritance).

Using the Typing Module in Dojo

This typing capability is available in the dojox.lang.typed module, and can be used just like the standalone “typed” function. In addition, this module integrates with Dojo’s class constructing convenience function, dojo.declare.

One feature that makes the dojox.lang.typed module convenient is that we don’t need to reassign the return value of typed to the class variable; we can simply do:

dojo.require("dojox.lang.typed");
dojox.lang.typed(dojo.declare("acme.Balloon", null, {
  setVolume: function(volume){
  ... method implementations
}));
acme.Balloon.properties = {
  ... property definitions  
};

Furthermore, we can automate the typing of all our classes by setting the Dojo configuration variable dojo.config.typeCheckAllClasses to true before loading dojox.lang.typed. Then all classes that are declared (after dojox.lang.typed is loaded) through dojo.declare will be typed.

Development/Production Distinction

The typing module is intended to be used for development, easing the construction of classes that need to work together by verifying invariant assumptions. It is highly recommended that type checking be removed for production code. Type checking adds extra bytes (to download the schemas) and has a performance hit (to perform all the type checks), which should be avoided for production. Fortunately, the unobtrusive design makes this very doable. The actual class implementations are unaffected by typing definitions, and as long as the application runs without any type errors, the application should behave exactly the same without type definitions as it does with them. With this design, type definitions can be stored separately from implementations and easily omitted for production code.

In Dojo, ensuring that typing information is omitted can easily be done with the build process. There are a couple of ways of doing this. First we can utilize the build’s excludeStart() function to remove typing information. Therefore we could define our Balloon class:

//>>excludeStart("typed", true);
dojo.config.typeCheckAllClasses = true;
dojo.require("dojox.lang.typed");
//>>excludeEnd("typed");

dojo.declare("acme.Balloon", null, {
  setVolume: function(volume){
  ... method implementations
});

//>>excludeStart("typed", true);
acme.Balloon.properties = {
  ... property definitions  
};
//>>excludeEnd("typed");

The build process will then strip out all the schema information.

While using excludeStart() is a simple way to exclude type information, it can be a little ugly. Alternately, we could have our schemas stored in separate modules, and then use build profiles to omit the typing modules. This can contribute to better implementation/interface separation.

acme/Balloon.js:

dojo.provide("acme.Balloon");
dojo.require("acme.Balloon-schema");

dojo.declare("acme.Balloon", null, {
  setVolume: function(volume){
  ... method implementations
});
acme/Balloon-schema.js:

dojo.provide("acme.Balloon-schema");
dojo.config.typeCheckAllClasses = true;
dojo.require("dojox.lang.typed");
acme.Balloon.properties = {
  ... property definitions  
};

Now in the build profile, we can include the schema modules as layerDependencies to completely exclude them from the build:

dependencies = {
	layers: [
		{
			name: "../acme/Balloon.js",
			layerDependencies : [
				"../acme/Balloon-schema.js"
			],
			dependencies: [
				"acme.Balloon"
			]
		},
...

Limitations and Potential

Type checking on property changes can not be performed in IE because this feature relies on JavaScript getters and setters. However (as noted before), this is a development-time tool, and full cross-browser support is not necessary to reap the benefits of type checking. Checking for invalid type assumptions—and utilizing typing information for integration of components between different developers—can be achieved during development on a subset of all browsers. Type checked applications will still work on IE; there just won’t be type checks performed on property changes. For production, when full-cross browser support is needed, it is recommended that type checks be eliminated anyway.

The JavaScript-based type checker is also limited in that it can’t intercept delete operations on properties. When a delete takes place, the setter is not fired, and the type checker is not able to reject the action (in the case of required properties). The type checker also cannot control the addition of new properties that have been declared (in the property definitions or prototype).

JSI-based type checking does not have any mechanism for defining types of local variables. This is intentional. Local variables rarely benefit from type information. Integration of components from different sources can define their interaction solely through public interfaces (which are typeable with JSI), and local type inference mechanisms can usually provide sufficient information about local variable types for IDEs and code analysis.

A more complete JSI-based type checking system can be seen in Persevere. Persevere implements JSI for type checking in the server-side JavaScript environment, and is able to reject delete and new property actions that do not conform to the schema. Typing in JavaScript has been a controversial idea due to the obtrusive nature of traditional typing annotations, but JSI allows gradual type constraints to be applied in a way that does not interfere with JavaScript’s simple syntax and object model.

This typing scheme has also been proposed to the ECMAScript working group. As noted before, the typing system proposed for edition 4 required extensive new syntax and relied significantly on namespaces, which have been rejected. A schema-based typing system is perhaps the most reasonable alternative for adding typing capabilities to ECMAScript, and provides significant benefits over the more static type systems that quash the interactive, dynamic nature of JavaScript. With the more gradual approach that is being taken to evolve the language, this typing system provides the perfect mechanism for preserving the familiar syntax and meta-programming abilities that exemplify JavaScript.

Portability and Metadata Retrieval

JSON schema interfaces are completely language-agnostic. This means that interfaces can be defined that apply to implementations in different languages. This can be particular useful for defining contracts in RPC-style situations. In fact, the SMD format, which is used extensively by Dojo for defining remote services, is essentially a remote service-oriented form of JSI.

One of the challenges with traditional class systems is finding a way to reify type information. This is a non-issue with JSI, since the type system is based completely on a runtime data structure that is readily available and already understood.

Typing in JavaScript

While there has been a lot of controversy over typing, only the most extreme would not be willing to concede that in certain situations, typing can be a valuable asset for defining contracts between components. JSI allows for more robust evolution of an application, faster detection of invariant violations that lead to hard-to-find bugs, and better information for integration of components and type-based tooling. Furthermore, leveraging JSON schema gives developers a very rich set of options for constraining values, much more than most type systems.

JSI gives us a standardized data structure for defining types—allowing for more robust large-scale application development, a better pathway for JavaScript tool evolution, clean implementation/interface separation, and an integrated, intuitive class/typing system. And it’s available in both the Dojo Toolkit and Persevere today!