The Practical Client

Exploiting TypeScript Arrays

TypeScript doesn't have the rich set of native collection classes that you're used to in the .NET Framework -- instead, it has just arrays and tuples. Fortunately, you can do quite a lot with them.

Compared to the .NET Framework with its rich toolbox of collections, TypeScript is poverty stricken. Fundamentally, TypeScript supports two kinds of collections: arrays (where all the members are of the same type and are accessed by position) and tuples (where each member can be of a different type). Arrays are probably the more familiar of the two types of collections so I'll look at them in this month's column and come back to tuples next month.

TypeScript Arrays
In TypeScript, arrays are themselves a data type, just like number and string). TypeScript provides quite a lot of ways for you to declare an array…but all of the methods give you the same result.

For example, you can declare an array using a "square bracket syntax" that will look very familiar to JavaScript developers. This example defines an array that can only hold Customer objects using that [] syntax:

var custs: Customer[];

You can also declare an array as an Array of some specific data type using a syntax that looks very much like C# generics. Like my previous example, this code also declares an array that can only hold Customer objects:

var custs: Array<Customer>;

You can also use TypeScript's union types to define an array that can hold several different types. This code defines arrays that can hold either Customer or CustomerNew objects (and only Customer and CustomerNew objects) using both syntaxes:

var custsExpanded: (Customer | CustomerNew)[];
var custsExpanded: Array<Customer | CustomerNew>;

Regardless of how you declare an array, you won't be able to use it until you initialize it. This code provides a very basic initialization:

custs = [];

This example initializes my array with two Customer objects using an array literal:

custs = [new Customer("A123"), new Customer("B456")];

You can also combine your array's declaration with its initialization:

var custs: Customer[]= [new Customer("A123"), new Customer("B456")];
var custs: Array<Customer> = [new Customer("A123"), new Customer("B456")];

I don't often refer to the JavaScript that is generated from my TypeScript code but, just to make the point that it doesn't matter what syntax you use, it's worthwhile to point out that both of those lines of TypeScript generate identical JavaScript:

var custs = [new Customer("A123"), new Customer("B456")];

And, regardless of which syntax you use for declaring an array, you'll find that the resulting objects have exactly the same methods and properties that you'd expect from a JavaScript array: length, pop, push, split, and so on. What you've defined is something that the TypeScript specification refers to as "array-like type" (anything that can be assigned to an array of the any type: any[]).

Controlling Functionality
If you'd prefer to limit what can be done with any particular array, you can define your own collection interface, provided it specifies some subset of the full Array functionality. This can be useful when, for example, you have a method that returns a collection and you want to limit what the client can do with your collection.

This example defines an collection of Customer objects by defining an interface with an indexer. The code then uses that interface to define a collection:

interface IArray {
    [position: number]: Customer;
}
var custs: IArray = [new Customer("A123"), new Customer("B456")];

As far as the TypeScript compiler is concerned, this custs collection will have no other members than the indexer defined in IArray (i.e. no length property, no push method, etc.). The only thing that can be done with this array is initialize it and access items in the collection by position using the indexer. That's what this code does:

var cust: Customer
cust = cust[0];

While this controls the operations that can be performed on the collection, it's probably a little extreme. For example, without a length property it's impossible to build a for loop to process this collection (by "impossible" I mean that I couldn't figure out how to do it). However, by adding more members of the array in the interface, you can selectively provide access to the members of the underlying JavaScript array. This example provides my collection with a length property and a push method:

interface IArrayExtended {
    [position: number]: Customer;
    length: number;
    push(item: Customer): number;
};

A collection defined with this interface would support processing in a for loop to (because it has a length property) and also adding items to the array (though the push method), as in this code:

private custs: IArrayExtended;

custs = [new Customer("A123"), new Customer("B456")];
this.custs.push(new Customer("C789"));

for (var i = 0; this.custs.length > i; i++) { }

Creating Custom Collections
You might be tempted to believe that you could leverage this technology to create a collection class with your own custom methods that supports an implicit indexer (I certainly was). As an example, you might want to create a Queue class that you could use like this:

var q: Queue<Customer>;
q = new Queue<Customer>();
q.enqueue(new Customer("A123");
q.enqueue(new Customer("A123");
for (var i = 0; q.length > i; i++)
{
    var cust = q[i];
}

You could create a class that would work like this…right up until the code attempts to access the class's indexer (the statement containing the expression q[i]). While it's easy to create an indexer in a class in Visual Basic or C# it's not an option in TypeScript. There is, of course, nothing stopping you from creating a itemAt method that would give you the functionality you want:

for (var i = 0; q.length > i; i++)
{
    var cust = q.itemAt(i);
}

Dictionaries
Retrieving items by position is useful. However, most languages also include dictionary collections where the indexer can be passed a key to retrieve the item in the dictionary assigned to that key. TypeScript allows you to define the interface that a dictionary collection requires: An indexer that accepts a string and returns the type of the items in the array. Here's what the interface for a dictionary of Customer objects looks like:

interface IDictionary {
    [key: string]: Customer;
};

Unfortunately, the TypeScript compiler will then constrain any other members in a collection that uses that interface to results that can be assigned to that indexer. Effectively, your dictionary can have only one member -- the indexer. This means that you can create your dictionary but you won't be able to define a length method that would allow you loop through the dictionary method to process all the members in a for loop.

Furthermore, a variable defined with this interface can not be assigned to a variable of type any[] so it's not one of TypeScript's "array-like types." It is, instead, an object and must be initialized using an object initializer (e.g. {}). This code uses my interface to create a dictionary for Customer objects, stores a Customer object in it, and then retrieves it:

var custsD: IDictionary;
custsD = {};
var cust: Customer;

custsD["A123"] = new Customer("A123");
cust = custsD["A123"];

However, because the positions in an array don't have to be contiguous, you might be better off to think of arrays in TypeScript dictionaries…just dictionaries with numerical keys. This code, which assigns items to "positions" 0 and 2 works just fine, for example:

this.custs = [];
this.custs[0] = new Customer("A123");
this.custs[3] = new Customer("B456");

Accessing this array using positions 0 through 3 would return a Customer object for positions 1 and 3 and the undefined type for positions 1 and 2. If you wanted to consider a TypeScript array as a dictionary with numeric keys (in this case, with two assigned keys: 0 and 3) that, rather than throwing an error, returns the undefined type when you use a key that isn't assigned, you wouldn't go far wrong. You can, for example, use negative values and strings as your array index for all of the collections I've defined including ones using my IArray interface:

this.custs[1] = new Customer("A123");
this.custs["A123"] = new Customer("A123");
Processing All Members with For-in

In other languages you may be used to using a For…Each construct that retrieves each item in collection and stops when the last item in the collection has been processed. TypeScript has a "syntactically" similar construct in the For-in statement…but it does something different. What the For-in statement does is process all the members of whatever it's given to process (for an object, you'll get a list of all the properties and methods in the object). This is where thinking of an array as a dictionary with numeric keys pays off.

For the collections I've defined in this column, the For-in loop returns the keys of the items in your collection rather than the items themselves. To use For-in with any of the collections (arrays or dictionaries) I've declared, you need to pass the variable used in the For-in loop to the indexer of the collection:

for (var c in this.custs) {
  cust = this.custs[c];
}

While more limited than other languages, TypeScript does give you arrays and a limited dictionary. But, as I said, arrays form only one half of the collections available in TypeScript -- the other half are tuples. As you'll see in next month's column, you can leverage tuples to create something more like the dictionary classes you're used to.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

Subscribe on YouTube