The Practical Client

Creating Type-Safe Structures (and Dictionaries) with Tuples

When classes are more structure than you need, tuples let you specify simple type-safe aggregates of other data types. They'll also let you create a dictionary collection…but it won't be type-safe.

In last month's column I showed how to declare and use TypeScript arrays. As I mentioned in that column, in many ways it's better to think of TypeScript arrays as dictionaries: Collections whose values can be accessed by providing a key value. It's just that, with arrays, the keys are always numeric values.

Tuples are very similar to Arrays but, while Arrays let you put together collections of similar objects, tuples let you put together collections of different objects. If, for example, you have a function that will return several classes you could define a new class that has properties to hold each of those classes. Or you could define a tuple that will do the same thing with less overhead.

For example, I might want a function to return a customer's Id, the related Customer object and the customer's credit limit. I could define a whole new class (perhaps called CustomerCreditInfo) but if I'm only using this data structure within one application (and, perhaps, in just one place in that application), a class seems like overkill -- a tuple will let me do the same thing. Because I can use a tuple without having to instantiate it, using tuples also saves me some overhead.

Tuple Basics
In TypeScript, a tuple is a data type that allows you to define what data can appear in each position in an array. Once you've defined your tuple, you can use it to declare a variable that will hold that data.

This type definition, for example, specifies a tuple with a string in the first position, a Customer object in the second position and a number in the third position:

type custCreditTuple = [string, Customer, number];

Under the hood, I've effectively defined a class with properties named 0, 1 and 2 (in fact, the TypeScript definition of a "tuple-like type" is that it has a property called 0). I can now use this type definition to declare a variable and then load all the positions in that variable with data using an array literal:

var custCreditInfo: custCreditTuple;
custCreditInfo = ["A123", new Customer("A123"), 2000];  

What's special about tuples is that TypeScript enforces the data type in each property/position. For example, if I access the second position in this tuple TypeScript knows that it's a Customer object and automatically gives me access to the properties on the Customer object:

var fName: string;
fName = custCreditInfo[1].FirstName;

Furthermore, if I assign a value to a tuple using a literal array then TypeScript ensures that I assign all the values for the tuple and use the right data type in each position. This code won't work because it doesn't provide a value for the third item in the tuple, for example:

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

I may not always want to load all the values in my tuple at once and, fortunately, I don't have to: I can also load the individual positions in my tuple. This code puts a string in the first position of my tuple:

custCreditInfo[0] = "A123"

Even when loading items one position at a time, TypeScript will ensure that I do the right thing: I'll get a compile-time error if I try to put a Customer object in the first position (which is supposed to be a string) or put a string in the third position (which is supposed to be a number).

Working Beyond the Limits
As with arrays, each item in a tuple is effectively assigned a key: the key for the first item is 0, the second item's key is 1, and the third item's key is 2. And, as with arrays, you can add items with arbitrary keys. If you go beyond the items you specified in the tuple then TypeScript will require you to use one of the types already defined in the tuple.

For example, my custCreditInfo tuple has three data types defined in its three positions: string, Customer and number. I can put something in position 100 of the tuple, but, if I do, TypeScript will insist that it must be one of those three data types. This code, for example, won't work because I'm trying to use something other than a string, Customer or number:

custCreditInfo[100] = true;

Similarly, if I pull data from a position past the ones defined in my tuple definition, I'm going to need to store the result in a variable that can hold any of my tuple's types (string, Customer or number). Of course, that's exactly what TypeScript's union types are for. This declaration defines a union type that supports all of the types in my tuple:

type custCreditInfoType = string | CustomerNew | number;

Using that union type I can define a variable with it, pull a value from a position beyond the end of my tuple and then use a type guard to determine what I can do with the value:

var custCreditInfoItem: custCreditInfoType;
custCreditInfoItem = custCreditInfo[100];
var fName: string;
if (custCreditInfoItem instanceof Customer) 
{
  fName = custCreditInfoItem.FirstName;
}

You can use a for…in loop to process all of the items in a tuple, remembering that the loop actually returns the keys for each item in the tuple. This example processes all of the items in my credit information tuple:

for (var itm in custCreditInfo)
{
  tupleItem = custCreditInfo[itm];
}

Finally, if I have a bunch of customer credit information to process there's no reason why I can't use my tuple definition to define an Array of tuples. For example, this code declares an Array of custCreditTypes and initializes it:

custCreditInfos: custCreditTuple[] = [];

The code to retrieve the Customer object from the third item in this array can be pretty ugly, though:

var cust: Customer
cust = custCreditInfo[3][1];

If you don't like using the type keyword, you can define a tuple using an interface that specifies the data type for keys 0, 1, 2. All of my code would work exactly the same way if, instead of defining custCreditTuple as a tuple, I'd defined it as an interface with properties called 0, 1 and 2 (though, if I'm using an interface, I should name this interface ICustCreditTuple to follow the TypeScript naming conventions). Here's what that alternative definition of my tuple would look like:

interface custCreditTuple 
{ 
  0: string, 
  1: Customer,
  2: number 
};

Defining a Dictionary
You can also use tuples to define a dictionary (a collection that allows you to store and retrieve items by key value). Your first step is to define a Dictionary tuple consisting of a string (the key) and a value (in this case, a Customer object):

type DictionaryItem = [string, Customer];

Now you can define an array of DictionaryItems and initialize it:

var custDictionary: DictionaryItem[];
custDictionary = [];

With those two declarations in place, you can store values in the dictionary by key and retrieve them by key, as this code does:

custDictionary["A123"] = new Customer("A123");
var cust: Customer;
cust = custDictionary["A123"];

This isn't very type-safe, though, at least at compile time: TypeScript doesn't object to me storing numbers in this dictionary, for example. TypeScript also doesn't recognize that the value returned from the dictionary is a Customer object so, while you can write code like this, you won't get any IntelliSense support for using the FirstName property:

fName = custDictionary["A123"].FirstName;

You'll have to be careful when using this dictionary that you don't do something that will generate a runtime error (asking for a property that doesn't exist on the class, for example).

If you think that you'll be using dictionaries that will store something other than Customer objects (or be using keys other than strings), you can create a generic DictionaryItem in two steps. First, use the interface method for defining your tuple:

interface DictionaryItem  
{ 
  0: string, 
  1: Customer 
};

Then convert your interface into a generic version by replacing actual data types with type markers (here, I've used K for my key data type and V for my value data type):

interface DictionaryItem<K,V>  
{ 
  0: K, 
  1: V 
};

Now, when you need a new dictionary, you can specify the data types you want to use this time:

var custDictionary: DictionaryItem<string, Customer>[];

However, because TypeScript doesn't currently enforce type-safety with these structures what you're actually providing here is documentation, not functionality. Still, there may come a day when TypeScript will pay better attention to these declarations (I wrote this column with TypeScript 1.5.3).

As I said in my earlier column on arrays, you don't get a lot of collections with TypeScript, but now you know how to use both of them.

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

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube