Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / Javascript

A simple JavaScript generic type pattern implementation

Rate me:
Please Sign up or sign in to vote.
4.75/5 (5 votes)
18 Apr 2010CPOL7 min read 36.9K   15   3
A simple JavaScript generic type pattern implementation

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)
{
    /// <param name="item" type="Number"/>
    /// <returns type="Number"/>
    
    if (!(item instanceof Number))
    {
        throw new Error("item must be of type 'Number'");
    }

    return this.innerList.push(item);
}

ListOfNumber.prototype.SetItem = function(index, item)
{
    /// <param name="index" type="Number"/>
    /// <param name="item" type="Number"/>
    
    if (!(item instanceof Number))
    {
        throw new Error("item must be of type 'Number'");
    }

    this.innerList[index] = item;
}

ListOfNumber.prototype.GetItem = function(index)
{
    /// <param name="index" type="Number"/>
    /// <returns type="Number"/>
    
    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:
 figure4.png
Figure 5:
 figure5.png
Note parameter and return types.


Figure 6:
 figure6.png
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)
{
    /// <param name="item" type=""/>
    
    /// <returns type="Number"/>

    //typecheck:item;

    return this.innerList.push(item);
}

ListOfNumber.prototype.SetItem = function(index, item)
{
    /// <param name="index" type="Number"/>
    
    /// <param name="item" type=""/>
    
    //typecheck:item;

    this.innerList[index] = item;
}

ListOfNumber.prototype.GetItem = function(index)
{
    /// <param name="index" type="Number"/>
    
    /// <returns type=""/>

    // we have to sacrifice the cast
    // which is of questionable value in any case.    
    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)
{
    /// <summary>
    /// Emits the source code for an object.
    /// The goal is to emit a block of code that can successfully reconsitute an object.
    /// It works in the controlled situations presented in this example. YMMV
    /// </summary>
    /// <param name="objName" type="String">The fully qualified typename of
    /// the object to dump</param>
    /// <returns type="String"></returns>

    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";
        }
    }
    
    // place the members after the prototype for obvious reasons
    return ctor + "\n\n" + prototype + "\n\n" + members;

}

function stringifyValue(value)
{
    /// <summary>
    /// Gets source code for an object.
    /// 
    /// Uses crockford's json2.js to get parsable source
    /// for value types.
    /// </summary>
    /// <param name="value" type="Object"></param>
    /// <returns type="String"></returns>

    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)
{
    /// <param name="item" type=""/>

    /// <returns type="Number"/>

    //typecheck:item;

    return this.innerList.push(item);
};
ListOfNumber.prototype.SetItem = function(index, item)
{
    /// <param name="index" type="Number"/>

    /// <param name="item" type=""/>

    //typecheck:item;

    this.innerList[index] = item;
};
ListOfNumber.prototype.GetItem = function(index)
{
    /// <param name="index" type="Number"/>

    /// <returns type=""/>

    // we have to sacrifice the cast
    // which is of questionable value in any case.    
    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)
{
    /// <summary>Declares a generic type</summary>
    /// <param name="sourceTypename" type="String">
    /// The fully qualified typename of the base type.
    /// Must exist and source code must contain XML comments that are compatible
    /// with generic.createSimple.
    /// </param>
    /// <param name="genericTypename" type="String">
    /// The fully qualified typename of the generic type to be declared. Must NOT exist.
    /// </param>
    /// <param name="elementTypename" type="String">
    /// The fully qualified typename of Queue's content. Must exists.
    /// </param>
    /// <returns type="Object">
    /// A typesafe generic version of source type with full intellisense support
    /// </returns>
    /// <remarks>
    /// <remarks>

    var source = getSource(sourceTypename);

    if (typeof (eval(elementTypename)) === undefined)
    {
        throw new Error("Cannot instantiate " + elementTypename);
    }

    // sourceTypename is likely to have dots. need to fix it up before
    // creating the regexp
    var sourceTypeExpression = new RegExp(sourceTypename.replace(/\./g, "\\."), "g");
    
    // a replacement pattern that emits a simple typechecking expression
    var typecheck = "\n    if (!($1 instanceof ~elementType~)){
        throw new Error(
            '$1 must be of type ~elementType~');};\n".replace(
            /~elementType~/g, elementTypename);

    // replace the object name, inject the type parameter typename into empty
    // 'type' attributes
    source = source.
            // replace the object name
            replace(sourceTypeExpression, genericTypename).
            // inject the type parameter typename into empty 'type' attributes
            replace(/type=""/g, 'type="' + elementTypename + '"').
            // inject a typechecking expression at sites of //typecheck:??;
            replace(/\/\/typecheck:(.*);/g, typecheck) + ";\n";

    // build a self executing function to declare the generic type
    var activator = "(function() {\n" + genericTypename + "=" + source + 
        " \nreturn " + genericTypename + ";})();";

    // the type will be instantiated in the current scope and is immediately available
    // but lets grab a reference to it and return it just for giggles
    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)
    {
        /// <param name="item" type="Date"/>

        /// <returns type="Number"/>


        if (!(item instanceof Date)) { throw new Error('item must be of type Date'); };


        return this.innerList.push(item);
    };
    ListOfDate.prototype.SetItem = function(index, item)
    {
        /// <param name="index" type="Number"/>

        /// <param name="item" type="Date"/>


        if (!(item instanceof Date)) { throw new Error('item must be of type Date'); };


        this.innerList[index] = item;
    };
    ListOfDate.prototype.GetItem = function(index)
    {
        /// <param name="index" type="Number"/>

        /// <returns type="Date"/>

        // we have to sacrifice the cast
        // which is of questionable value in any case.    
        return this.innerList[index];
    };
    return ListOfDate;
})();

Finally, some visual confirmation:

Figure 12: 

figure12.png

Figure 13:

figure13.png

Figure 14:

figure14.png

Figure 15:

figure15.png

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Salient Solutions
United States United States
My name is Sky Sanders and I am an end-to-end, front-to-back software solutions architect with more than 20 years experience in IT infrastructure and software development, the last 10 years being focused primarily on the Microsoft .NET platform.

My motto is 'I solve problems.' and I am currently available for hire.

I can be contacted at sky.sanders@gmail.com

Comments and Discussions

 
GeneralUpdate Panel fails to expand vertical menu in Master Page Pin
SteveMets6-Oct-09 11:23
professionalSteveMets6-Oct-09 11:23 
GeneralRe: Update Panel fails to expand vertical menu in Master Page Pin
Sky Sanders16-Dec-09 7:45
Sky Sanders16-Dec-09 7:45 
GeneralRe: Update Panel fails to expand vertical menu in Master Page Pin
SteveMets17-Dec-09 16:47
professionalSteveMets17-Dec-09 16:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.