Building a Dictionary in TypeScript


Introduction: JavaScript to TypeScript

It only takes a quick search of “JavaScript” in r/ProgrammerHumor to understand the love/hate relationship that software developers have with JavaScript. The language boasts dynamic types, which therefore creates a lack of type-safety, often leading to weird and wonderful results. One of the subreddit’s favourite examples is:

[6, -2, 2, -7].sort();

returns

[-2, -7, 2, 6]

Weird or wonderful? You decide.

TypeScript is a superset of JavaScript, expanding upon the popular language—we’ve been using it as standard at Sinara for web development for several years now. It brings (optional) static typing to JavaScript, which we’ve found helps ease .NET developers into front-end development in HTML5. TypeScript compiles to JavaScript, so it works everywhere that JavaScript does. Even though the destination is ultimately the same, TypeScript provides an easier passage in your journey to a working web-based front-end. It also helps to keep you from falling victim to JavaScript’s questionable approach to basic maths!

Over the years, we’ve encountered masses of instances where it’s been necessary to store and transfer data in key-value pairs. In the past, there was no data structure provided that could fulfil this requirement, though this was resolved with the introduction of ES6’s Map. However, in projects that utilise JSON for data-transfer; for example, SignalR communication between server and clients; the Map data-structure is dangerous. It cannot be stringified via the normal pathway; you’d need to work around this; ultimately leaving you with an object to stringify. This leaves developers to build their own dictionary using JavaScript’s dynamic typing, a challenging task.

This article will give an example of a way to mimic a dictionary in TypeScript, a useful workaround if you’re unable to use the ES6 Map. Other approaches exist, all with their advantages and disadvantages. But if your sole need is quick access by key, I’ve found this to work very well.

Example in C#

Let’s imagine we have a website, similar to many that we develop for our clients here at Sinara. The users of the website all have different permissions for how much they’re allowed to do, and these permissions are controlled by the roles assigned to the user. One user may have multiple roles, like trader and supervisor, whilst another might only have the role readonly.

Suppose the client also employs site administrators, who are in charge of the day-to-day running of the website, including user management. The site administrators need to be able to review and update the permissions of the users. In order to do this, they will expect to be able to select a username and see the list of roles that they’ve been assigned.

What we need here is a mapping, or dictionary, of username to roles, or roles by username. That is, we want to be able to store roles by a username key. When the administrator clicks on a username, we want to be able to quickly access the roles of the selected user.

In C#, this is easily done. The Generic Dictionary is one of the most frequently used classes in the .NET Framework, and to create a new one we simply declare and instantiate it:

var rolesByUsername = new Dictionary<string, List<Role>>();

The key for the dictionary is a string (the username), and the value assigned to each key will be a List<Role> (a list of role objects).

Assuming we’ve populated this dictionary correctly, we can obtain the list of roles for a given username using the indexer:

List<Role> rolesForUser = rolesByUsername["sam"];

By placing a key into the indexer of a dictionary, we receive the corresponding value.

Example in TypeScript

If ES6’s Map isn’t suitable, we can use TypeScript to mimic the role of a dictionary. Instead of relying on existing types in the language, we take advantage of our ability to define objects on-the-fly.

Below, we define a variable ‘rolesByUsername’, and using TypeScript, we declare the data type:

var rolesByUsername: { [username: string]: Role[] } = {};

Our variable above, rolesByUsername, has a fairly complicated type definition. Let’s break it down.

We’ve specified an index signature (labelled ‘username’), dictating that the index is a string. We’ve also defined a type for anything accessed using the index: the value must be an array of Role objects.

Note that naming our index ‘username’ is purely cosmetic. Clarity is king: we’re making sure that it’s obvious what we’re storing the roles by.

To populate our imitation dictionary, we could do something like the following (assuming we have an array called ‘users’ that we have obtained via a web service/SignalR call):

for (let i = 0; i < users.length; ++i){
    let user: User = users[i];
    rolesByUsername[user.username] = user.userRoles;
}

In each iteration, we’re telling TypeScript that the value at index ‘user.username’ is the array of Roles contained in user.userRoles. There is no requirement to add a key and then provide the value: we create the key and assign it a value in the same step.

Later, when we need to access the roles for a user, we use the indexer with the username as the key (just as in C#):

var rolesForUser: Role[] = rolesByUsername['sam'];

It’s worth noting that whilst this can be accessed like a C# dictionary, the object is built completely differently. Had we wished to, we could also have accessed the roles for user ‘sam’ using the following code:

var rolesForUser: Role[] = rolesByUsername.sam;

Let this sink in. When we ‘populated’ our imitation dictionary, we were actually re-defining the object on-the-fly. We specified that the object had a property, ‘sam’, which is an array of Role objects. In C#, the class definition would have looked something like this:

public class RolesByUsername
{
    public Role[] Sam { get; set; }
    public Role[] Tom { get; set; }
}

Each time we add a ‘key’a property with that name is added to the object definition under the hood.

It is wise to note that constantly redefining the object definition is costly, which is a significant disadvantage of this mock dictionary. If you’re building a performant web app, this could be a troublesome method if you’re planning to build a large dictionary.

Taking a step back, it’s possible you need your dictionary to be indexed by a user ID of some form. What happens if we try to create a dictionary with a number as the key?

rolesByUsername[user.userId] = user.userRoles;

Behind the scenes, this will actually cast the number (userId) to a string. In most scenarios, you won’t notice any difference, but it’s worth being aware of the mysterious work that JavaScript is conducting behind your back.

Conclusion

So, there we have it. Quick access to specific results, just what the doctor ordered. These imitation dictionaries can be especially useful in filtered dropdowns in HTML forms. If the chosen item in the first dropdown dictates the available values in the next dropdown, you can quickly repopulate the latter if you’ve built an ‘optionsBySelection’ dictionary.

Using an object as a dictionary does come with some fairly considerable disadvantages…checking if the dictionary is populated (or empty) can be computationally costly, as can adding new keys. With each key that’s added, browsers like Chrome will recompile their definition of the object. This wouldn’t be noticeable when playing with a small array of items, but larger dictionaries would likely encounter some significant performance issues and you’d almost certainly want to avoid this particular method.

In the next post, I will see how we can improve on this by using the ES6 map.

Share