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
1is converted to the string"1". - Inherited names are visible.
'toString' in {}istrue, 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,10and2,Object.keysgives['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, orundefinedwhen the key is missing.has(key)returnstrueorfalse.delete(key)removes the entry and returns whether anything was removed. Deleting a missing key is harmless.sizeis 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.
- Create the map. Declare
fruitsas aMap<string, number>and pass the starting entries to the constructor as an array of[key, value]pairs: apples 3, bananas 5, grapes 2. - Add four apples. Read the count with
get, add 4 and store it back withset. The!is safe here because "apples" was just inserted. - Report missing pears. If
fruits.has("pears")is false, printOut of pears. - Remove the bananas with
delete("bananas"). - Print the inventory. Loop over the map with
for...of, unpacking each pair intofruitandcount, 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"] = 7is 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. Usesetandget. - Treating
undefinedas a number. A missing key givesundefined, andundefined + 1isNaN. Check withhasor 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 onlyundefinedandnull. - Reading
sizeas a method. It isfruits.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 withhasor??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.