Map object in javascript or typescript can be used as a key-value object like a tuple. It’s almost the same as Dictionary type in C#. Let’s master how to use Map object.
Basic usage of Map object
Map type can store any data. This is basic usage.
const map = new Map<string, number>();
const one = 1;
let two = 2;
const three = 3;
map.set("one", one)
.set("two", two)
.set("three", three);
console.log(`size: ${map.size}`);
console.log(map);
console.log("before update: " + map.get("two"));
two = 16;
console.log("after update: " + map.get("two"));
// ---- Result ----
// size: 3
// Map(3) { 'one' => 1, 'two' => 2, 'three' => 3 }
// before update: 2
// after update: 2
map.set
function returns Map object, so its function call can be chained if you like.
In the example above, the variable two
is updated but its change doesn’t affect the value stored in map object.
Storing Object type in Map object
Map can store object type too but we should know that the value change to the original variable affects the value stored in Map object. Let’s see the example.
const hoge: Person = { name: "hoge", age: 12 };
const foo: Person = { name: "foo", age: 22 };
const map = new Map<string, Person>();
map.set("hoge", hoge);
map.set("foo", foo)
console.log(`size: ${map.size}`);
console.log(map.get("hoge"));
hoge.name = "Updated hoge";
console.log(map.get("hoge"));
// ----Result----
// size: 3
// { name: 'hoge', age: 12 }
// { name: 'Updated hoge', age: 12 }
hoge.name
is updated and its change affects the value stored in Map! Object type is stored as a reference to the variable. Therefore, its value change to the original variable affects the value stored in Map object. Be careful.
If it is an object type and you don’t want to have the side effect you need to copy the original object by using the spread operator.
map.set("foo", { ...foo })
console.log(map.get("foo"));
foo.name = "updated foo";
console.log(map.get("foo"));
// ---- Result ----
// { name: 'foo', age: 22 }
// { name: 'foo', age: 22 }
It works but let’s see another example.
let hoge = { name: "hoge", age: 12, deep: { value: 99 } };
const map = new Map<string, Person>();
map.set("hoge", { ...hoge });
hoge.deep.value = 1;
// ---- Result ----
// { name: 'hoge', age: 12, deep: { value: 1 } }
The value change to hoge.deep.value
affects the result because the spread operator is a shallow copy. It copies only first-level values. It copies the reference for the second or deeper level.
It can’t be used for class either because it doesn’t copy a getter or function. Use one of the modules that create a complete copy. cloneCopy() from lodash is one of the options for that.
Using Map object instead of switch-case or if-else
Map can be used instead of if-else and switch-case because Map object can’t have the same key.
const map = new Map([
[0, "Initializing"],
[1, "Idle"],
[2, "Running"],
[3, "Stop"],
[4, "Error"],
]);
getStatus(0);
getStatus(3);
getStatus(5);
function getStatusBySwitch(value: number) {
switch (value) {
case 0: return "Initializing";
case 1: return "Idle";
case 2: return "Running";
case 3: return "Stop";
case 4: return "Error";
default: "Undefined";
}
}
function getStatus(value: number) {
console.log(`${getStatusBySwitch(value)}, ${map.get(value)}`);
}
// ---- Result ----
// Initializing, Initializing
// Stop, Stop
// Undefined, undefined
Its result is the same. This is the powerful way when many cases are required, for example in the following cases
- When a function wants to create a new class depending on the input
- When many constant variables are necessary
Using Array function (reduce, filter, map, etc…)
Map object implements iterator instead of Array. Therefore, forEach
function can be used but sometimes we want to use Array functions like reduce
, filter
,map
. However, those functions can’t be called directly. We need to convert it to Array first if we want to use those functions.
const map = new Map([
["Desk", 44],
["Chair", 23],
["Light", 36],
["Mat", 97],
]);
map.forEach((value: number, key: string) => {
console.log(`${key}: ${value}`);
});
const sum = Array.from(map.values())
.reduce((acc, cur) => acc + cur);
const ave = sum / map.size;
console.log(`sum: ${sum}, ave: ${ave}`);
const joinedKeys = Array.from(map.keys()).join(",");
console.log(joinedKeys);
// ---- Result ----
// Desk: 44
// Chair: 23
// Light: 36
// Mat: 97
// sum: 200, ave: 50
// Desk,Chair,Light,Mat
Performance comparison, Map, if-else, switch
Are you interested in performance? I measured it in this simple example.
import { performance } from "perf_hooks";
{
const statusMap = new Map([
[0, "Initializing"],
[1, "Idle"],
[2, "Running"],
[3, "Stop"],
[4, "Error"],
]);
function getStatusByMap(value: number) {
return statusMap.get(value);
}
function getStatusBySwitch(value: number) {
switch (value) {
case 0: return "Initializing";
case 1: return "Idle";
case 2: return "Running";
case 3: return "Stop";
case 4: return "Error";
default: return "Undefined";
}
}
function getStatusByIfElse(value: number) {
if (value === 0) {
return "Initializing";
} else if (value === 1) {
return "Idle";
} else if (value === 2) {
return "Running";
} else if (value === 3) {
return "Stop";
} else if (value === 4) {
return "Error";
}
}
function measure(cb: (value: number) => void) {
const start = performance.now();
for (let i = 0; i < 100000000; i++) {
const val = i % 5;
cb(val);
}
const end = performance.now();
console.log(`${cb.name}: ${end - start}`);
}
measure(getStatusByMap);
measure(getStatusByIfElse);
measure(getStatusBySwitch);
}
The result is the following. The unit is ms.
Map | If-Else | Switch |
---|---|---|
517.1828000005335 | 1212.4598000003025 | 1353.3245999999344 |
494.4639999996871 | 1207.6337999999523 | 1012.4100999999791 |
764.1765000000596 | 1510.5867000008002 | 1148.4190999995917 |
575.7749999994412 | 1263.11429999955 | 1286.8793999999762 |
There is no big difference between if-else and switch but Map is 2 times faster. However, this loop count is not in practice. I changed it from 100,000,000 to 100,000. The result is the following.
Map | If-Else | Switch |
---|---|---|
3.665999999269843 | 6.266699999570847 | 1.847099999897182 |
3.836600000038743 | 5.293499999679625 | 1.8441000003367662 |
4.10120000038296 | 4.5566999996080995 | 1.888600000180304 |
The switch case is the fastest but This tiny difference doesn’t cause any performance problems. I think Javascript and Typescript are not used for performance-sensitive software. Let’s write in the cleanest way.
Extra test
I added the number of conditions from 5 to 100.
const statusMap = new Map([
[0, '0'],
[1, '1'],
[2, '2'],
...
[99, '99'],
]);
function getStatusByMap(value: number) {
return statusMap.get(value);
}
function getStatusBySwitch(value: number) {
switch (value) {
case 0: return '0';
case 1: return '1';
case 2: return '2';
...
case 99: return '99';
default: return "Undefined";
}
}
function getStatusByIfElse(value: number) {
if (value === 0) { return "0"; }
else if (value === 1) { return '1' }
else if (value === 2) { return '2' }
...
else if (value === 99) { return '99' }
}
function measure(cb: (value: number) => void) {
const start = performance.now();
for (let i = 0; i < 100000000; i++) {
const val = i % 100;
cb(val);
}
const end = performance.now();
return end - start;
}
function run() {
const a = measure(getStatusByMap);
const b = measure(getStatusByIfElse);
const c = measure(getStatusBySwitch);
console.log(`${a}|${b}|${c}`);
}
for (let i = 0; i < 10; i++) {
run();
}
100 M times
Map | If-Else | Switch |
---|---|---|
623.3642000006512 | 3021.0782000003383 | 3108.6668999996036 |
1850.9503999995068 | 2985.6550000002608 | 2928.4054000005126 |
1734.608800000511 | 2934.537600000389 | 3064.6140000000596 |
1833.8176999995485 | 2977.3677000002936 | 2985.345900000073 |
1864.1046000001952 | 3062.9769999999553 | 3109.095200000331 |
1887.8833999997005 | 4253.697500000708 | 4481.461799999699 |
4189.872700000182 | 6373.2303999997675 | 6872.441399999894 |
5618.228700000793 | 7557.449599999934 | 6910.5358999995515 |
3710.831299999729 | 5727.273499999195 | 4694.466000000015 |
1982.4616000000387 | 3348.7060000002384 | 4472.591199999675 |
100 K times
Map | If-Else | Switch |
---|---|---|
6.649799999780953 | 7.819099999964237 | 6.39780000038445 |
2.237200000323355 | 2.590000000782311 | 2.590199999511242 |
4.67920000012964 | 4.40749999973923 | 4.497000000439584 |
4.129099999554455 | 3.210200000554323 | 3.1229999996721745 |
4.5604999996721745 | 4.666399999521673 | 3.496199999935925 |
3.5476999999955297 | 4.765700000338256 | 4.807300000451505 |
2.3504999997094274 | 3.7663000002503395 | 3.546000000089407 |
3.331500000320375 | 3.0432000001892447 | 4.181400000117719 |
1.7209999999031425 | 2.71100000012666 | 3.557099999859929 |
1.6250999998301268 | 2.724299999885261 | 3.3985999999567866 |
Well… It doesn’t make a big difference. Download the source code from my repository if you want to try it yourself.
Comments