Published on

TypeScript: Map Utilities

Table of Contents

The Map implementation in JavaScript is simpler than the HashMap implementation in Java.

In Java, the HashMap implementation uses hash codes and buckets to store and retrieve key-value pairs efficiently. This means that when you want to update a value for a key in a HashMap, you need to first check if the key is present, calculate its hash code, and then locate the corresponding bucket. Once you have the bucket, you need to iterate through its linked list to find the key-value pair and update it.

For example the compute method in Java's HashMap provides a way to avoid some of this complexity by combining the operations of checking if a key is present and updating its value. It also allows you to define the update operation as a function, which can be useful in some cases.

In contrast, the Map implementation in JavaScript is based on a simple key-value store, where keys are compared using the === operator. When you want to update a value for a key in a Map, you can simply call map.set(key, value) and the existing value (if any) will be overwritten.

So while the compute method in Java's HashMap provides a way to simplify and optimize certain types of operations, it's not strictly necessary in a simpler implementation like JavaScript's Map.

Map compute function

ts-map-compute.tsx
interface ComputeMap<K, V> extends Map<K, V> {
  compute(key: K, remappingFunction: (oldValue?: V, key?: K) => V): V;
}

class ComputeMap<K, V> extends Map<K, V> {
  compute(key: K, remappingFunction: (oldValue?: V, key?: K) => V): V {
    const oldValue = this.get(key);
    const newValue = remappingFunction(oldValue, key);
    this.set(key, newValue);
    return newValue;
  }
}

In this example, we declare an interface ComputeMap that extends the built-in Map interface,

and adds a compute() method with the same signature as Java's HashMap.compute().

A class ComputeMap that implements this interface and provides an implementation for the compute() method.

compute() function takes a key and a remappingFunction as arguments.

The remappingFunction takes the oldValue and key as optional arguments and returns a new value for the key.

compute() function first gets the oldValue for the key using this.get(key), and then calls the remappingFunction to get the newValue.

It then sets the newValue for the key using this.set(key, newValue), and returns the newValue.

Here's an example of how to use compute() method with our custom ComputeMap class:

ts-map-compute.tsx
const map = new ComputeMap<string, number>();
map.set('key1', 1);
map.set('key2', 2);

map.compute('key1', (oldValue) => (oldValue || 0) + 1);
// Returns 2
// Adds 1 to the existing value for 'key1'

map.compute('key2', (oldValue) => (oldValue || 0) + 1);
// Returns 3
// Adds 1 to the existing value for 'key2'

map.compute('key3', () => 1);
// Returns 1
// Sets the value for 'key3' to 1

console.log(map);
// Log: ComputeMap { 'key1' => 2, 'key2' => 3, 'key3' => 1 }

Here, we have created a new ComputeMap and added some key-value pairs with map.set(). By calling map.compute() to update the value for key1 and key2, and set the value for key3. The remappingFunction adds 1 to the existing value for key1 and key2, and sets the value to 1 for key3.

Map computeIfAbsent function

ts-map-compute-if-absent.ts
interface ComputeIfAbsentMap<K, V> extends Map<K, V> {
  computeIfAbsent(key: K, mappingFunction: (key?: K) => V): V;
}

class ComputeIfAbsentMap<K, V> extends Map<K, V> {
  computeIfAbsent(key: K, mappingFunction: (key?: K) => V): V {
    if (!this.has(key)) {
      const newValue = mappingFunction(key);
      this.set(key, newValue);
      return newValue;
    }
    return this.get(key)!;
  }
}

Here, we declare an interface ComputeIfAbsentMap that extends the built-in Map interface, and we added a computeIfAbsent() method with the same signature as Java's HashMap.computeIfAbsent(). Then we have defined a class ComputeIfAbsentMap that implements this interface and provides an implementation for the computeIfAbsent() method.

The computeIfAbsent() function takes a key and a mappingFunction as arguments. The mappingFunction takes the key as an optional argument and returns a new value for the key. The computeIfAbsent() method first checks if the map contains the key using this.has(key). If the key is not present, it calls the mappingFunction to get the newValue, sets the newValue for the key using this.set(key, newValue), and returns the newValue. If the key is already present, it returns the existing value for the key using this.get(key).

Here's an example of how to use the computeIfAbsent() method with our custom ComputeIfAbsentMap class:

ts-map-compute-if-absent.ts
interface Member {
  fullName: string
  subscriptionID?: number
}

const map = new ComputeIfAbsentMap<string, Member>();
map.set('member1', { fullName: 'John Smith' });

const member2 = map.computeIfAbsent('member2', (key) => {
  console.log(`Creating a new member with key '${key}'...`);
  return { fullName: 'Jane Doe', subscriptionID: 123 };
});
// Output: Creating a new member with key 'member2'...
// Returns { fullName: 'Jane Doe', subscriptionID: 123 }
// Adds the new key-value pair to the map

const member1 = map.computeIfAbsent('member1', (key) => {
  console.log(`Creating a new member with key '${key}'...`);
  return { fullName: 'John Doe', subscriptionID: 456 };
});
// Returns { fullName: 'John Smith' }
// Does not modify the existing key-value pair in the map

console.log(map);
// Log: ComputeIfAbsentMap {
//   'member1' => { fullName: 'John Smith' },
//   'member2' => { fullName: 'Jane Doe', subscriptionID: 123 }
// }

Here, we create a new ComputeIfAbsentMap and add a key-value pair using map.set(). We then call map.computeIfAbsent() to get the value for 'member2', which is not present in the map. The mappingFunction creates a new Member object and returns it. The resulting map has the new key-value pair. We then call map.computeIfAbsent() again to get the value for 'member1', which is already present in the map. The mappingFunction is not called, and the existing value is returned.

Map computeIfPresent function

ts-map-compute-f-present.ts
interface ComputeIfPresentMap<K, V> extends Map<K, V> {
 computeIfPresent(key: K, remappingFunction: (key: K, value: V) => V): V | undefined;
}

class ComputeIfPresentMap<K, V> extends Map<K, V> implements ComputeIfPresentMap<K, V> {
  public computeIfPresent(key: K, remappingFunction: (key: K, value: V) => V): V | undefined {
      const value = this.get(key);

    if (value !== undefined) {
        const newValue = remappingFunction(key, value);
        this.set(key, newValue);
    return newValue;
    }
  return undefined;
  }
}


const myMap: ComputeIfPresentMap<number, string> = new ComputeIfPresentMap<number, string>();
myMap.set(1, 'one')
myMap.set(2, 'two')

myMap.computeIfPresent(1, (key, value) => value + ' plus one')
myMap.computeIfPresent(3, (key, value) => 'three')

console.log(myMap.get(1)); // Log: 'one plus one'
console.log(myMap.get(2)); // Log: 'two'
console.log(myMap.get(3)); // Log: undefined

The computeIfPresent function takes two parameters: the key to look up and the remappingFunction to apply if the key is present in the Map. The remappingFunction takes the key and the existing value for that key, and returns the new value to be stored in the Map.

The first call to computeIfPresent updates the value for key 1 by appending ' plus one' to the existing value ('one').

The second call to computeIfPresent has no effect since key 3 is not present in the Map.

Note that this implementation of computeIfPresent works in a similar way to compute and computeIfAbsent functions we discussed earlier, but only applies the remapping function if the key is already present in the Map. If the key is not present, the function returns undefined without modifying the Map.

Map replaceAll function

ts-map-replace-all.ts
interface ReplaceAllMap<K, V> extends Map<K, V> {
  replaceAll(callbackfn: (value: V, key: K, map: Map<K, V>) => V): this;
}

class ReplaceAllMap<K, V> extends Map<K, V> implements ReplaceAllMap<K, V> {
  public replaceAll(callbackfn: (value: V, key: K, map: Map<K, V>) => V): this {
    for (const [key, value] of this.entries()) {
      const newValue = callbackfn(value, key, this);
      this.set(key, newValue);
    }
    return this;
  }
}

const myMap: ReplaceAllMap<number, string> = new ReplaceAllMap<number, string>();
myMap.set(1, 'one')
myMap.set(2, 'two')

myMap.replaceAll((value, key) => value.toUpperCase() + ' ' + key)
     .forEach((value, key) => console.log(value))
     // Log: 'ONE 1'
     // Log: 'TWO 2'

Here, we define the ReplaceAllMap interface, which extends the built-in Map interface and adds the replaceAll method signature. Then we define a CustomMap class that extends the built-in Map class and implements the ReplaceAllMap interface.

The replaceAll method implementation iterates over each entry in the Map, applies the callback function to the value and key of each entry, and sets the new value in the Map.

Note that the replaceAll method returns the modified Map instance to allow for chaining.

GET the full HashMap library ‼️

ts-hash-map.ts
interface IHashMap<K, V> {
  clear(): void;
  delete(key: K): boolean;
  forEach(callbackfn: (value: V, key: K, map: IHashMap<K, V>) => void): void;
  get(key: K): V | undefined;
  has(key: K): boolean;
  set(key: K, value: V): this;
  size: number;
  keys(): IterableIterator<K>;
  values(): IterableIterator<V>;
  entries(): IterableIterator<[K, V]>;
  [Symbol.iterator](): IterableIterator<[K, V]>;
  merge(other: IHashMap<K, V>, deep?: boolean): this;
  compute(key: K, callbackfn: (value: V, key: K, map: Map<K, V>) => V): this;
  computeIfAbsent(key: K, mappingFunction: (key: K) => V): V;
  computeIfPresent(key: K, mappingFunction: (key: K, value: V) => V): V | undefined;
  replace(key: K, oldValue: V, newValue: V): boolean;
  getOrDefault(key: K, defaultValue: V): V;
  replaceAll(callbackfn: (value: V, key: K, map: IHashMap<K, V>) => V): this;
}

class HashMap<K, V> implements IHashMap<K, V> {
  private readonly map: Map<K, V>;

  constructor() {
    this.map = new Map<K, V>();
  }

  public clear(): void {
    this.map.clear();
  }

  public delete(key: K): boolean {
    return this.map.delete(key);
  }

  public forEach(callbackfn: (value: V, key: K, map: IHashMap<K, V>) => void): void {
    this.map.forEach((value, key) => {
      callbackfn(value, key, this);
    });
  }

  public get(key: K): V | undefined {
    return this.map.get(key);
  }

  public has(key: K): boolean {
    return this.map.has(key);
  }

  public set(key: K, value: V): this {
    this.map.set(key, value);
    return this;
  }

  public get size(): number {
    return this.map.size;
  }

  public keys(): IterableIterator<K> {
    return this.map.keys();
  }

  public values(): IterableIterator<V> {
    return this.map.values();
  }

  public entries(): IterableIterator<[K, V]> {
    return this.map.entries();
  }

  [Symbol.iterator](): IterableIterator<[K, V]> {
    return this.map[Symbol.iterator]();
  }

  public merge(other: IHashMap<K, V>, deep?: boolean): this {
    for (const [key, value] of other.entries()) {
      if (deep && value instanceof Object) {
        const currentValue = this.get(key);
        if (currentValue instanceof Object) {
          this.set(key, { ...currentValue, ...value });
        } else {
          this.set(key, value);
        }
      } else {

        this.set(key, value);
      }
    }

    return this;
  }

 public compute(key: K, callbackfn: (value: V, key: K, map: Map<K, V>) => V): this {
    const currentValue = this.get(key);
    if (currentValue !== undefined) {
      const newValue = callbackfn(currentValue, key, this.map);
      this.set(key, newValue);
    }
    return this;
  }

 public computeIfAbsent(key: K, mappingFunction: (key: K) => V): V {
    if (!this.map.has(key)) {
      const value = mappingFunction(key);
      this.map.set(key, value);
      return value;
    }

    return this.map.get(key)!;
  }

 public replace(key: K, oldValue: V, newValue: V): boolean {
    if (this.map.has(key) && this.map.get(key) === oldValue) {
      this.map.set(key, newValue);
      return true;
    }

    return false;
  }

 public getOrDefault(key: K, defaultValue: V): V {
    if (this.map.has(key)) {
      return this.map.get(key)!;
    }

    return defaultValue;
  }

 public computeIfPresent(key: K, mappingFunction: (key: K, value: V) => V): V | undefined {
    if (this.map.has(key)) {
      const newValue = mappingFunction(key, this.map.get(key)!);
      this.map.set(key, newValue);
      return newValue;
    }

    return undefined;
  }

  public replaceAll(callbackfn: (value: V, key: K, map: IHashMap<K, V>) => V): this {
    for (const [key, value] of this.entries()) {
      const newValue = callbackfn(value, key, this);
      this.set(key, newValue);
    }
    return this;
  }
  public toString(): string {
    const entries = Array.from(this.entries());
    const entriesString = entries.map(([key, value]) => `${key} => ${value}`).join(', ');
    return `Map (${this.size}) {${entriesString}}`;
  }
}