Start learning free

LESSON 236 OF 348 · HASH TABLE

Hash Maps in TypeScript: Map and Plain Objects

Lesson Read the full explanation

Hash Tables, better known as Hash Maps, are just collections of key-value pairs. In other words, they are pieces of data (values) mapped to unique identifiers called properties (keys).

Hash Maps were designed to give us a way of, given a key, associating a value with it for very quick lookups.

In JavaScript, an Object is implemented as a Hash Map behind the scenes. This is why adding and lookup of an Object's properties is so fast.

If you've worked with Objects in the past, you were working with Hash Maps without knowing it.


Creation:

You create an empty Hash Map just by creating an empty Object, like this:

let hashMap = {};

Creating an empty Hash Map takes O(1) time.


Initialization:

You can also add some key-value pairs in your Hash Map at creation:

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown'
};

Accessing values:

A value is retrieved from a Hash Map by specifying its corresponding key in square brackets ([]):

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown'
};

// Accessing values:
console.log(person['name']); // "Andy"
console.log(person['age']); // 30

// Accessing non-existing key:
console.log(person['job']); // undefined

As you can see, if you refer to a key that is not in the Hash Map, JavaScript returns undefined.

Accessing a value from a Hash Map takes O(1) time.


Adding an entry:

Adding an entry to an existing Hash Map is simply a matter of assigning a new key and value, via the assignment operator:

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown'
};

// Adding new entries:
person['job'] = 'Teacher';

let str = 'hobby';
person[str] = 'Fishing';

console.log(person);
/* The new Hash Map:
{
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown',
	'job': 'Teacher',
	'hobby': 'Fishing'
	
} */

Adding a new entry to a Hash Map takes O(1) time.


Updating an entry:

If you want to update an entry, you can just assign a new value to an existing key:

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown'
};

// Updating entries:
person['name'] = 'Andrew';
person['age'] = 29;

console.log(person);
/* The new Hash Map:
{
	'name': 'Andrew',
	'age': 29,
	'hair color': 'brown'
} */

Updating an entry to a Hash Map takes O(1) time.


Deleting entries:

The delete operator allows you to remove a key from a Hash Map:

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown',
	'hobby': 'Fishing'
};

// Deleting entries:
delete person['name'];

let key = 'age';
delete person[key];

// Deleting a non-existent entry:
delete person['job']; // does nothing

console.log(person);
/* The new Hash Map:
{
	'hair color': 'brown',
	'hobby': 'Fishing'
} */

The Hash Map first checks if that key exists and if not, it does nothing and also doesn't raise an error, so it's safe to delete non-existing keys.

Deleting an entry from a Hash Map takes O(1) time.


Checking if a key exists:

You can check if a key exists in a Hash Map using the in operator:

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown',
	'hobby': 'Fishing'
};

let key = 'age';
console.log(key in person); // prints "true"

console.log('job' in person); // prints "false"

Checking if a key exists in a Hash Map takes O(1) time.


Iterating over the map keys:

If we want to iterate over keys of the dictionary, we can use the for loop along with the in operator:

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown',
	'hobby': 'Fishing'
};

for (let key in person) {
    console.log(key + " is " + person[key]);
}

// This will print the following:
// name is Andy
// age is 30
// hair color is brown
// hobby is Fishing

Iterating over a dictionary takes O(n) time.


Iterating over the dictionary values:

If we want to iterate over values of the map, we can use the for loop along with the of operator and the Object.values(map) function:

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown',
	'hobby': 'Fishing'
};

for (let val of Object.values(person) {
    console.log(val);
}

// This will print the following:
// Andy
// 30
// brown
// Fishing

Iterating over a dictionary takes O(n) time.


Iterating using both the key and the value for simplicity:

If we want to iterate over both keys and values of the map, we can use the for loop along with the of operator and the Object.entries(map) method:

let person = {
	'name': 'Andy',
	'age': 30,
	'hair color': 'brown',
	'hobby': 'Fishing'
}

for (let [key, val] of Object.entries(person)) {
    console.log(key + ' is ' +  val);
}

// This will print the following:
// name is Andy
// age is 30
// hair color is brown
// hobby is Fishing

Notice the order is not the same as initiated. Dictionary keeps the data in random order

Iterating over a dictionary takes O(n) time.


Assignment
Follow the Coding Tutorial and let's play with some Hash Maps.


Hint
Look at the examples above if you get stuck.


STEP 1 OF 5
    Ctrl + Enter to check

    TYPESCRIPT · HASH TABLE

    Hash Map in TypeScript, explained

    What a hash map is

    A hash map (also called a hash table or dictionary) stores key-value pairs. You give it a key and it gives back the value stored under that key, without scanning through the other entries. Internally it turns the key into a number (a hash) that points to where the entry lives. That is why adding, reading, updating, deleting and checking for a key all take constant time on average, O(1). Walking over every entry takes O(n).

    Reach for a hash map when you need to ask "have I seen this before?" or "how many of these are there?" quickly: counting occurrences, looking up an item by name, or remembering the index of a value you passed earlier.

    TypeScript has two common ways to build one. A plain object, {}, works as a map from string keys to values. The built-in Map class is made for this job, and it is what this lesson's exercise uses.

    A plain object as a map

    let person = {
        'name': 'Andy',
        'age': 30,
        'hair color': 'brown'
    };
    
    person['job'] = 'Teacher';       // add an entry
    person['age'] = 29;              // update an entry
    console.log(person['name']);     // Andy
    console.log(person['pet']);      // undefined (missing key)
    console.log('age' in person);    // true
    delete person['hair color'];     // remove an entry
    

    Reading a missing key gives undefined, not an error. Deleting a missing key does nothing. Objects have limits, though:

    • Keys are always strings (or symbols). A number key such as 1 is converted to the string "1".
    • Inherited names are visible. 'toString' in {} is true, because every object inherits that method.
    • Order is not always insertion order. Keys that look like whole numbers come first, in ascending order, and the rest follow in insertion order. For an object with the keys b, a, 10 and 2, Object.keys gives ['2', '10', 'b', 'a'].

    Map: the built-in hash map

    Map avoids those problems. Its keys can be any type, it has no inherited keys, it remembers insertion order, and it keeps its own size. In TypeScript you give the types of the keys and the values in angle brackets: Map<string, number>.

    const stock = new Map<string, number>();
    stock.set("b", 1);
    stock.set("a", 2);
    stock.set("b", 9);           // overwrites the value for "b"
    
    console.log(stock.get("a")); // 2
    console.log(stock.get("z")); // undefined
    console.log(stock.has("b")); // true
    console.log(stock.size);     // 2
    console.log([...stock.keys()]); // [ 'b', 'a' ]
    

    The main methods:

    • set(key, value) adds the entry, or replaces the value if the key exists.
    • get(key) returns the value, or undefined when the key is missing.
    • has(key) returns true or false.
    • delete(key) removes the entry and returns whether anything was removed. Deleting a missing key is harmless.
    • size is the number of entries. It is a property, not a method.

    Iterating with for...of gives [key, value] pairs in insertion order, and you can unpack each pair directly in the loop header.

    The ! after get

    In TypeScript's strict mode, get is typed as returning number | undefined, because the key might be missing. Adding to it, as in stock.get("a") + 4, is then a compile error. When you know the key exists, the non-null assertion ! tells the compiler to treat the value as a number: stock.get("a")! + 4. It checks nothing at run time. Types are erased when TypeScript compiles to JavaScript, so if the key were missing, undefined + 4 would give NaN.

    The exercise, step by step

    The lesson tracks how many fruits of each type are in stock.

    1. Create the map. Declare fruits as a Map<string, number> and pass the starting entries to the constructor as an array of [key, value] pairs: apples 3, bananas 5, grapes 2.
    2. Add four apples. Read the count with get, add 4 and store it back with set. The ! is safe here because "apples" was just inserted.
    3. Report missing pears. If fruits.has("pears") is false, print Out of pears.
    4. Remove the bananas with delete("bananas").
    5. Print the inventory. Loop over the map with for...of, unpacking each pair into fruit and count, and print one line per entry.
    const fruits: Map<string, number> = new Map([
        ["apples", 3],
        ["bananas", 5],
        ["grapes", 2],
    ]);
    
    fruits.set("apples", fruits.get("apples")! + 4);
    
    if (!fruits.has("pears")) {
        console.log("Out of pears");
    }
    
    fruits.delete("bananas");
    
    for (const [fruit, count] of fruits) {
        console.log("We have " + count + " " + fruit + ".");
    }
    

    It prints:

    Out of pears
    We have 7 apples.
    We have 2 grapes.
    

    Apples went from 3 to 7, the bananas are gone, and the remaining entries come out in the order they were inserted.

    Common mistakes

    • Using brackets on a Map. fruits["apples"] = 7 is accepted under loose compiler settings, but it sets a property on the Map object, not an entry: fruits.get("apples") still returns 3. Strict mode reports it as an error. Use set and get.
    • Treating undefined as a number. A missing key gives undefined, and undefined + 1 is NaN. Check with has or give a default with ??, as in (fruits.get("pears") ?? 0) + 1.
    • Using || for a default. || replaces any falsy value, so a stored count of 0 would also be replaced. ?? replaces only undefined and null.
    • Reading size as a method. It is fruits.size, without parentheses.

    Key takeaways

    • A hash map gives constant-time average lookup, insertion, update and deletion by key.
    • Map<K, V> is the purpose-built hash map: set, get, has, delete, size, and iteration in insertion order.
    • A missing key gives undefined, so guard with has or ?? before doing arithmetic.
    • Plain objects work as maps too, but their keys are strings and numeric-looking keys are ordered first.
    • The ! operator only silences the compiler. It never changes what the program does.
    Keep going with TypeScriptAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free