download source code
download this article in .doc format.
Overview
In a recent article I presented a simple implementation of a generic type system in JavaScript. Please see that document for a detailed enumeration of the motivations and benefits, including type safetly and Visual Studio intellisense support.
In this document I will present a 'better' implementation that eliminates many of the limitations of my 'simple' generic type implementation.
I will also provide a starter library of generic compatible collections including Queue, ListArray and Dictionary that you may use immediately and that can serve as a reference for implementing your own generic types in JavaScript.
Features:
- Full Visual Studio 2008 JavaScript Intellisense design time support.
- Multiple type parameters.
- Run time type safety.
Limitations
- No constraint implementation.
Implementing a full type system is beyond the scope of my intentions so it will be left to the user to document proper usage of a generic type. - Complex, nested object hierarchies or objects with novel construction strategies may not be suitable as candidates for a generic type.
- Only one generation is supported. e.g. You may not declare a generic type with a base class that has, itself, been generated as a generic type.
This is a somewhat artificial limitation that I have imposed by removing parameter type and typecheck tokens upon creation.
A more robust parsing engine could remove this limitation but certain browser limitations would narrow the scope of the base functionality.
Usage
Type parameter declaration for input parameters and return types is accomplished via the use of comments in a format compatible with Visual Studio 2008 XML Comment documentation.
This has the added benefit of enhancing the design time experience in Visual Studio but in no way limits the usage of the generic type system to Visual Studio.
Listing 1 presents the full prototype of the included dictionary implementation. It has been adorned with XML comments making it consumable by the generic type system and with 'void' tags to indicate to the generic compiler where to emit typechecking expressions.
Dynamically creating an entire type checking expression is necessary as the parameter type is not known until compile type and various types require different comparison strategies.
A generic factory method has been added as a property of the function itself. The usage of the generic type system can be inferred from the comments in this method as well as the following listing.
Note that the object is fully functional as is and that no functional changes have been made to the source code so it may be used as an untyped dictionary as-is. The same is true of the other included collection classes.
Listing 1: XML comment and void tag usage
Dictionary.prototype.getAllKeys = function()
{
return this._getAllKeys();
};
Dictionary.prototype.contains = function(key)
{
void ("typecheck:key:$K$;");
return this._contains(key);
};
Dictionary.prototype.getItem = function(key)
{
void ("typecheck:key:$K$;");
return this._get(key);
};
Dictionary.prototype.putItem = function(key, value)
{
void ("typecheck:key:$K$;");
void ("typecheck:value:$V$;");
this._put(key, value);
};
Dictionary.prototype.removeItem = function(key)
{
void ("typecheck:key:$K$;");
return this._remove(key);
};
Dictionary.createGenericType = function(keyType, valueType, typeName)
{
if (typeof (salient.generic) === "undefined")
{
throw salient.util.createException("salient.collections.Dictionary.createGenericType", "TypeLoadException", "salient.generic is not loaded");
}
return salient.generic.createGenericType("salient.collections.Dictionary", { $K$: keyType, $V$: valueType }, typeName);
};
Listing 2: salient.generic.createGenericType()
salient.generic.createGenericType = function(sourceTypename, typeParams, genericTypename)
{
}
To declare a generic Dictionary of key:String, value:RegExp you would use the following expression.
Listing 3: Generic Dictionary of String,RegExp
salient.generic.createGenericType("Dictionary", { $K$: "String", $V$: "RegExp" }, "RegExpDictionary");
The type 'RegExpDictionary' is now available for instantiation and use.
Listing 4: Examples of emitted typecheck expressions
if ((typeof (element) !== 'undefined' || element !== null) && typeof (element) !== 'boolean')
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', 'element must be boolean value');
};
if ((typeof (element) !== 'undefined' || element !== null) && typeof (element) !== 'number')
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', 'element must be a number');
};
if ((typeof (element) !== 'undefined' || element !== null) && typeof (element) !== 'string')
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', 'element must be a string');
};
if ((typeof (element) !== 'undefined' || element !== null) && !(element instanceof Date))
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', 'element must be of type Date');
};
if ((typeof (element) !== 'undefined' || element !== null) && !(element instanceof RegExp))
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', 'element must be of type RegExp');
};
if ((typeof (element) !== 'undefined' || element !== null) && !(element instanceof Error))
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', 'element must be of type Error');
};
if ((typeof (element) !== 'undefined' || element !== null) && !(element instanceof salient.collections.ArrayList))
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', ' element must be of type salient.collections.ArrayList');
};
Typecheck expressly ignores null and undefined values and only throws an exception if the object being checked is not null or undefined and is not of the indicated type. It is left to the implementor of the generic type to guard null as is appropriate for the method.
The current type checks are, as you can see, rather strict. It is on my TODO list to add a flag to the void tag to indicate some degree of implicit type conversion. e.g. Number to String and numeric string literals to Number.
Simple List Example Revisited
In the interest of continuity I will refit the prima facia example from the previous article, List, as a generic type.
Listing 5: Simple List with generic type paramter
var List = function()
{
if (arguments[0] && (arguments[0] instanceof List))
{
return arguments[0];
}
this.innerList = [];
}
List.prototype.AddItem = function(item)
{
void ("typecheck:item:$V$;");
return this.innerList.push(item)
}
List.prototype.GetItem = function(index)
{
return this.innerList[index];
}
List.prototype.SetItem = function(index, item)
{
void ("typecheck:item:$V$;");
this.innerList[index] = item;
}
salient.generic.createGenericType("List", { $V$: "Number" }, "ListOfNumbers");
Listing 6: Generated Source for ListOfNumbers
ListOfNumbers = function()
{
if (arguments[0] && (arguments[0] instanceof ListOfNumbers))
{
return arguments[0];
}
this.innerListOfNumbers = [];
}
ListOfNumbers.prototype.AddItem = function(item)
{
if ((typeof (item) !== 'undefined' && item !== null) && typeof (item) !== 'number')
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', 'item must be a number');
};
return this.innerListOfNumbers.push(item)
};
ListOfNumbers.prototype.GetItem = function(index)
{
return this.innerListOfNumbers[index];
};
ListOfNumbers.prototype.SetItem = function(index, item)
{
if ((typeof (item) !== 'undefined' && item !== null) && typeof (item) !== 'number')
{
throw salient.util.createException('ListOfNumbers', 'ArgumentException', 'item must be a number');
};
this.innerListOfNumbers[index] = item;
};
ListOfNumbers.__baseType = "List";
ListOfNumbers.__typeName = "ListOfNumbers";
ListOfNumbers.__typeParams = { "$V$": "Number" };
ListOfNumbers.__class = true;
And the obligatory illustrations:
Figure 1:
Figure 2:
Figure 3:
Figure 4:
Figure 5:
Figure 6:
Deep Type Parameters
Deep type parameter declaration simply involves the direct usage of a token in the body of a method. The usage of this technique renders the base class unsuitable for use in any context other than as a generic base type.
Listing 7: Deep Type Parameters
AList.prototype.putItem = function(name, color)
{
var item = new $E$(name, color);
this.add(item);
};
The requirements of a use case may find value in this technique. For instance, a generic dictionary implementation that uses an anonymous generic key/value pair as it's element type. For now, I leave it to the reader and will not discuss it further in this document.
Browser/Platform Support Challenges
My inital strategy was to implement all features via comments in source code. Implementation went fine and I wrote a batch of unit tests which all seemed to pass in every browser/os combination I could wrangle.
Until.
Until I wrote the negative tests. These are tests for expected exceptions. Negative tests are as important, if not more as evidenced by this case, than postitive tests.
In my negative tests I pass arguments of invalid types expecting an ArgumentException. In every browser based on the Mozilla engine all of my negative tests failed as no exception was thrown.
So I checked the generated source code and found all of the typecheck expressions missing. The only way this could happen is if the tokens are not in the source. Guess what?
The JavaScript implementation used by all Mozilla based browsers does not provide ANY access to the actual source. The toString and toSource provide the compiled source sans comments and unused string literals.
The generic type system will emit valid source code but without comments from which to glean typecheck tags they are basically clones of the base type. This means no runtime type checking.
This does not affect the design time benefits of intellisense in Visual Studio, which I feel are tremendous, but I could see all of my work in implementing a generic type system in JavaScript ending up as a novel idea with no practical application. But I was not giving up without a fight.
The problem of no comments in the source really only affects the typechecking tokens. Intellisense is of use only to people devving in VS and of course VS uses an IE engine for it's code editors.
I investigated a few strategies and settled on a compromise. Type checking expressions were initially emitted at the location of a comment formatted as follows:
//typecheck:key:$K$;
As noted, this token is not to be found on mozilla browsers. The final implementation, which satisfies every major and most minor browsers on Win/Mac/Linux, is as follows:
void ("typecheck:item:$V$;");
Currently typechecking is strict. I hope to implement a flag to enable some degree of implicit conversion.
Browser Compatibility Matrix
I think it is clear from the tests results below that this is a viable cross browser library.
Fully Compatible
These browsers/platforms pass the test suite fully.
- Linux Arora 0.5
- Linux Conkeror
- Linux Firefox 3.0.13
- Linux Galeon 2.0.6
- Linux MidBrowser
- Linux Midori 0.1.2
- Linux Opera 9.64
- Linux Web Browser 2.26.1
- MacOS Firefox 3
- MacOS Firefox 3.5
- MacOS Flock 2.5.2
- MacOS OmniWeb 5.9.2
- MacOS OmniWeb622.6
- MacOS Opera 9.64
- MacOS Safari 2.0.4
- MacOS Shiira 2.2
- MacOS iCab 4.6.0
- Windows Avant Browser 11.7
- Windows Chrome 2
- Windows Crazy Browser 3.0.0 rc
- Windows Explorer 6
- Windows Explorer 7
- Windows Explorer 8
- Windows Firefox 3.013
- Windows Firefox 3.09
- Windows Firefox 3.5
- Windows MaxThon 2.5.5
- Windows Opera 9.64
- Windows Safari 3.2
- Windows Safari 4
- Windows Sleipnir 2.8.3
- Windows SlimBrowser 4.12
Partial Compatibility
There are no comments or voids in source so no runtime type checks. Valid compilable code, intellisense support but NO runtime typechecking.
- Mozilla 1.8 - 2
- Linux Seamonkey 1.1.17
- MacOS Camino 1.6.8
- MacOS SeaMonkey 1.1.17
- Windows K-Meleon 1.5.3
- Windows Seamonkey 1.1.17
- Windows Firefox 2
Total Failure
These C grade browsers failed to even load the scripts
- Linux Dillo
- Linux NetSurf
- Windows Amaya 11.1.2
- Windows Opera 8.54
Implementation Details
Though the implementation is not exactly trivial, the concept is a fairly simple and revolves around manipulating the source code of an object with regular expressions and then compiling it with a call to eval.
- Ensure the constituent types exist and are instantiatable and that the new generic typename does not exist.
- Get the source code for the base type.
- get the constructor source
- append the source for all prototype members
- append the source for all instance members
- Replace, globally, the base type's name with the new generic typename.
- Replace, globally, type parameter tokens with their new specific type names.
- Replace, globally, void ("typecheck"); tags with type checking expressions that are suitable for the type.
- Scan for type/typeparam pairs, copy value of typeparam to type and delete typeparam attribute.
- evaluate the resultant source code in the current scope.
In The Box
- A zip file containing only the source files for this article.
- salient.generic.exerpt.js
Contains the various namespaces and classes to support generic type creation as well as a small set of standard collection types; Queue, ArrayList and Dictionary. These classes each have a convenience method, createGenericType(), which simplifies the declaration.
You will also find a KeyValue pair class that is generic compatible that you may find of use.
NOTE: the namespaces contained in this file have been truncated to only the classes necessary to support the generic type system. A full release of salientJS is forthcoming. - demo.js
Contains our friend 'List' and the generic declaration of 'ListOfNumbers'. You can experiment with type creation in this file and immediately visualize the results in the script tag of demo.htm (or another script that references demo.js). - tests.htm/tests.js
A suite of unit tests, leveraging jsunittest.js, that can serve as usage examples and guidelines for the included collection types and the createGenericType method. - reflection.htm
This is a simple tool that accepts a typename and will dump the source code. Is useful for debugging or perhaps you would like to generate hardcopy source for a strongly typed class to be used without the need for a reference to the generic typing system.
- A Visual Studio 2008 solution containing the same source files ready for exploration of design time type creation and intellisense support.
Conclusion
The use of generic types can improve the consistency and quality of your code by introducing typesafe classes and at the same time reduce surface area and testing requirements by eliminating duplicate source code.
An added benefit for Visual Studio 2008 users is the rich intellisense support that results as a happy (and intended) side effect of leveraging the XML Comment format.
This package can also serve as a code generation tool with the use of reflection.htm. The generated source code has only a single reference to the framework code, generation of the type checking exception, that can simply be replaced with an Error constructor.
Remember that generics are not only for collection classes. There are countless other applications for generics, even in a rudimentary implementation such as salient.generic.