Handling Undefined Key Values in Data Structures

In a lot of algorithm problems we use a hash table pattern to keep count of something as we iterate or traverse through a data structure. It is a very common scenario to have a situation that goes as follow:

  1. If key exists in data structure, e.g. Map, Object, Set, Array, increment the value, or add to the existing value, modify the value, etc. Any variation of that
  2. If key does not exist in data structure, initiate it with whatever default value you need. E.g. if your counting you want to initiate each non-existent key with a 0 (or 1) value so you can just simply increment it later.

There are various ways of approaching this. We will show off some of them here:

If-else

We use a basic if-else statement and check if hash[element] evaluates to false, then use the ! operator to flip that to true. This only happens if the key we were looking for was not define in the object yet

let arr = ['a', 'b', 'c', 'a', 'd'];
let obj: Record<string, number> = {};
let map: Map<strinig, number> = new Map();
let set: Set<number> = new Set();
let list: [string, number][] = []

for (let elem of arr) {
    if (!obj[elem]) {
        obj[elem] = 1;
    } else {
        obj[elem] += 1;
    }
}

for (let elem of arr) {
    if (!(elem in obj)) {
        obj[elem] = 1;
    } else {
        obj[elem] += 1;
    }
}
// { a: 2, b: 1, c: 1, d: 1 }

for (let elem of arr) {
    if (!map.has(elem)) {
        map.set(elem, 1)
    } else {
       map.set(elem, map.get(elem) + 1)
    }
}
// Map(4) { 'a' => 2, 'b' => 1, 'c' => 1, 'd' => 1 }

for (let elem of arr) {
    if (!set.has(elem)) {
        set.add(elem)
    } else {
        // do whatev you want
    }
}
// Set(4) { 'a', 'b', 'c', 'd' }

Ternary Operator

for (let elem of arr) {
  	obj[elem] = obj[elem] ? obj[elem] + 1 : 1;
}

for (let elem of arr) {
  	obj[elem] = elem in obj ? obj[elem] + 1 : 1;
}
// { a: 2, b: 1, c: 1, d: 1 }

for (let elem of arr) {
    map.set(elem, map.has(elem) ? map.get(elem) + 1 : 1)
}
// Map(4) { 'a' => 2, 'b' => 1, 'c' => 1, 'd' => 1 }

for (let elem of arr) {
  set.has(elem) ? null : set.add(elem)
}
// Set(4) { 'a', 'b', 'c', 'd' }

Or || operator

for (let elem of arr) {
  	obj[elem] = obj[elem] + 1 || 1 
}
// { a: 2, b: 1, c: 1, d: 1 }

for (let elem of arr) {
    map.set(elem, map.has(elem) + 1 || 1)
}
// Map(4) { 'a' => 2, 'b' => 1, 'c' => 1, 'd' => 1 }

for (let elem of arr) {
  set.add(elem)
}
// Set(4) { 'a', 'b', 'c', 'd' }

Using a Proxy object

We can intercept any method of a object by creating a proxy. This is great to create default values when accessing a property that does not yet exist.

let handler: ProxyHandler<Record<string, number>> = {
    get: (target: Record<string, number>, key: string) =>
        key in target ? target[key] += 1 : target[key] = 1,
};

let proxy = new Proxy(obj, handler);

for (let elem of arr) {
  	proxy[elem]
}
// Proxy [ { a: 2, b: 1, c: 1, d: 1 } ]