download source and demo from my blog
What are 'generic types'?
A generic type is defined using one or more type variables and has one or more methods that use a type variable as a placeholder for an argument or return type. For example, the type java.util.List<E> is a generic type: a list that holds elements of some type represented by the placeholder E. This type has a method named add(), declared to take an argument of type E, and a method named get(), declared to return a value of type E. source
Why attempt this in JavaScript?
My answer is two-fold. Primarily for type safety. While the development environment I work in, Visual Studio 2008, does does background compilation of JavaScript source files it does not perform type checking.
This leads me to the second compelling reason to implement a generic type pattern in JavaScript: Intellisense.
IntelliSense is Microsoft's implementation of autocompletion, best known for its use in the Microsoft Visual Studio integrated development environment. In addition to completing the symbol names the programmer is typing, IntelliSense serves as documentation and disambiguation for variable names, functions and methods using reflection. source
First approach: Single type parameterization of a functional base class
To paraphrase the definition quoted above, the purpose of generic types is to enable the reuse of code and guarantee type safety by means of applying one or more type parameters to a common, 'generic' base class and subsequently guarding input arguments and casting return values.
In this implementation I will generate a generic type with a single type parameter using a functional class as the base. By functional I mean that the class compiles and is usable in it's original state. I will simply add type safety and 'casting' to a copy of the class.
I say 'casting' in quotes because for this implementation, the primary reason for 'casting' is to enable intellisense. To truly cast a return value from an arbitrary method in JavaScript is just horrific to comtemplate.
Luckily there is no need for that. If we guard the inputs and the logic of the class is sound the return values should be of the proper type.
For guarding input arguments I will rely on an arbitrary comment, //typecheck:argname; , to indicate where to inject a type checking expression.
In addition I will also look for <param name="xx" type=""/> tags with an empty 'type' attribute and inject the type parameter. This will provide self documentation and dev-time cues for the programmer.
My primary goal in this implementation is to enable fully chained intellisense support for the generated type and this can be accomplished by the use a <returns type="xx"/> xml comment tag. This tag will cause visual studio to treat the product of the method as type 'xx'.
In this implementation I will be relying on these specific comments already being present in the source of the base class. At a later date I will take a look at injecting these comments dynamically when needed.
NOTE: This strategy involves no functional modifications to the source code of a class. Any class with compatible code can be retrofitted with comments to become a generic base class without altering it's behavior or breaking other code.
Consider this simple list class:
Figure 1:
var List = function()
{
this.innerList = [];
}
List.prototype.AddItem = function(item)
{
return this.innerList.push(item)
}
List.prototype.GetItem = function(index)
{
return this.innerList[index];
}
List.prototype.SetItem = function(index, item)
{
this.innerList[index] = item;
}
A type safe implementation of this class with Number as the element type would look something like this:
Figure 2:
var ListOfNumber = function()
{
this.innerList = [];
}
ListOfNumber.prototype.AddItem = function(item)
{
if (!(item instanceof Number))
{
throw new Error("item must be of type 'Number'");
}
return this.innerList.push(item);
}
ListOfNumber.prototype.SetItem = function(index, item)
{
if (!(item instanceof Number))
{
throw new Error("item must be of type 'Number'");
}
this.innerList[index] = item;
}
ListOfNumber.prototype.GetItem = function(index)
{
return Number(this.innerList[index]);
}
Notes on Figure 2:
- The guarding of the input arguments is perfect. Simple and exactly what is needed at runtime. For design time we are missing an opportunity to provide documentation and cues to the programmer.
- The casting of the return value is nice, especially if your return type is of one of the three types for which a casting constructor is available: String, Number and Boolean. And again, we are missing an opportunity to provide valuable information to the programmer.
Lets adorn this class with xml comments that will enhance the design time experience in visual studio and then examine the benefits of these adornments.
Figure 3:
var ListOfNumber = function()
{
this.innerList = [];
}
ListOfNumber.prototype.AddItem = function(item)
{
if (!(item instanceof Number))
{
throw new Error("item must be of type 'Number'");
}
return this.innerList.push(item);
}
ListOfNumber.prototype.SetItem = function(index, item)
{
if (!(item instanceof Number))
{
throw new Error("item must be of type 'Number'");
}
this.innerList[index] = item;
}
ListOfNumber.prototype.GetItem = function(index)
{
return Number(this.innerList[index]);
}
With the addition of these comments we now get Intellisense to indicate the types of arguments and returns. The VS Intellisense engine also uses the <returns/> type to enable chaining for writing fluent code. For more capable description with more pretty pictures see Scott Guthrie's blog post.
Figure 4:
Figure 5:
Note parameter and return types.
Figure 6:
Note that item is being treated as a Number by Visual Studio.
Ok, now the ListOfNumber class provides type safety and casting of return values for intellisense and chaining.
The goal of this exercise it to enable the dynamic generation of a List, or any other class, for any item type at compile time (and design time with VS) without the need to write any code.
We will now walk through the steps required to achieve this goal.
Implementation
First we need to abstract the base class and remove any hard references to the element type and provide a mechanism for replacing them at generation time.
Figure 7:
var ListOfNumber = function()
{
this.innerList = [];
}
ListOfNumber.prototype.AddItem = function(item)
{
return this.innerList.push(item);
}
ListOfNumber.prototype.SetItem = function(index, item)
{
this.innerList[index] = item;
}
ListOfNumber.prototype.GetItem = function(index)
{
return this.innerList[index];
}
Notes on Figure 7:
- Notice that I have removed the value of the 'type' attribute of the <param/> and <returns/> tags.
- Notice that I have replaced the type checking expressions with place holder tags indicating name of the parameter to check.
- Notice that I have dropped the Number() cast in the GetItem method. This would be very very difficult to implement in a generic scenario and is of no real value in any case. If the item is not of the proper type there is a logical error that needs fixed.
And finally, notice, if you have not already, that this listing is the same as Figure 1 with the addition of some xml comments. I have run through these steps to demonstrate that any base class with a certain code shape can be retrofitted with xml comments and become a generic base class.
Next we need some form of reflection to get the source code of the List so that we can munge it, applying the type parameter by injecting the typename into the <param/> and <returns/> tags and injecting a type checking expression at the site of any //typecheck:xx; comments.
Reflection in JavaScript
Fortunately for us, the Object.prototype.toString() as implemented on Function returns the source code of the function. To get the complete source code for an object we simply need to call .toString() on the constructor and iterate the constructor's properties and the constructor's prototype's properties calling .toString() on each. Concatenate these strings and you have compilable source code for the object. There are exceptions and limitation but for our general use case they should not become an issue. I am aware of the .toSource() ECMA method but it is not consistently implemented across platforms.
Figure 8:
function getSource(objName)
{
var obj = eval(objName);
if (typeof (obj) === undefined)
{
throw new Error("Cannot instantiate " + objName);
}
var ctor = obj.toString();
var key, value;
var members = "";
for (key in obj)
{
if (obj.hasOwnProperty(key))
{
value = obj[key];
members += "\n" + objName + "." + key + "=" + stringifyValue(value) + ";\n";
}
}
var prototype = "";
for (key in obj.prototype)
{
if (obj.prototype.hasOwnProperty(key))
{
value = obj.prototype[key];
prototype += objName + ".prototype." + key + "=" + stringifyValue(value) +
";\n";
}
}
return ctor + "\n\n" + prototype + "\n\n" + members;
}
function stringifyValue(value)
{
var valueType, valueSource;
valueType = typeof (value);
switch (valueType)
{
case "function":
valueSource = value.toString();
break;
default:
valueSource = JSON.stringify(value);
}
return valueSource;
}
Calling getSource("ListOfNumber") returns the following code which is a faithful representation of the original object.
Figure 9:
function()
{
this.innerList = [];
}
ListOfNumber.prototype.AddItem = function(item)
{
return this.innerList.push(item);
};
ListOfNumber.prototype.SetItem = function(index, item)
{
this.innerList[index] = item;
};
ListOfNumber.prototype.GetItem = function(index)
{
return this.innerList[index];
};
Now we just need to do some simple regular expression replacements to emit source code that can be evaluated resulting in a typesafe List with full Visual Studio Intellisense support.
Figure 10:
function createSimple(sourceTypename, genericTypename, elementTypename)
{
var source = getSource(sourceTypename);
if (typeof (eval(elementTypename)) === undefined)
{
throw new Error("Cannot instantiate " + elementTypename);
}
var sourceTypeExpression = new RegExp(sourceTypename.replace(/\./g, "\\."), "g");
var typecheck = "\n if (!($1 instanceof ~elementType~)){
throw new Error(
'$1 must be of type ~elementType~');};\n".replace(
/~elementType~/g, elementTypename);
source = source.
replace(sourceTypeExpression, genericTypename).
replace(/type=""/g, 'type="' + elementTypename + '"').
replace(/\/\/typecheck:(.*);/g, typecheck) + ";\n";
var activator = "(function() {\n" + genericTypename + "=" + source +
" \nreturn " + genericTypename + ";})();";
var result = undefined;
eval("result=" + activator);
return result;
};
If we call createSimple("ListOfNumber","ListOfDate","Date") a strongly typed List of Date will be declared in the current scope and be immediately available for use with full intellisense support.
This is the code that is generated and evaluated by createSimple("ListOfNumber","ListOfDate","Date"):
Figure 11:
(function()
{
ListOfDate = function()
{
this.innerList = [];
}
ListOfDate.prototype.AddItem = function(item)
{
if (!(item instanceof Date)) { throw new Error('item must be of type Date'); };
return this.innerList.push(item);
};
ListOfDate.prototype.SetItem = function(index, item)
{
if (!(item instanceof Date)) { throw new Error('item must be of type Date'); };
this.innerList[index] = item;
};
ListOfDate.prototype.GetItem = function(index)
{
return this.innerList[index];
};
return ListOfDate;
})();
Finally, some visual confirmation:
Figure 12:
Figure 13:
Figure 14:
Figure 15:
NOTE: createSimple() is not limited to native value types as type parameters. You may supply any object or class.
CAVEAT: Due to the way that the Visual Studio 2008 JavaScript background compiler and Intellisense engine operate you do not get always get full intellisense support for types that are declared in the file you are editing. A strategy that I use is to declare my library types in .js files and reference them via a <script/> tag in the html file or via a // <reference/> comment in .js files. See the sample project for more information.
What's Next?
The next logical step in this exercise is to devise a strategy for generating generic types with an arbitrary number of type parameters. A use case is to facilitate generic maps or dictionaries. e.g. Dictionary<String,MyClass>.
Stay tuned for part 2.