Extending the Map

A map is an abstract data type composed of key-value pairs. JavaScript comes with the built-in Map object that allows creating a map easily.

const myMap = new Map();map.set("foo", "bar");console.log(map.get("foo"));
const myMap = new Map<string, string>();map.set("foo", "bar");console.log(map.get("foo"));

Pretty easy, isn't it?

However, the methods available on a Map are very limited, namely:

  • Map.prototype.clear()
  • Map.prototype.delete()
  • Map.prototype.entries()
  • Map.prototype.forEach()
  • Map.prototype.get()
  • Map.prototype.has()
  • Map.prototype.keys()
  • Map.prototype.set()
  • Map.prototype.values()

In this article, you'll be guided to extend the Map object and add more methods for ease of use. The following are the list of methods we will be implementing:

  • find()
  • filter()
  • map()
  • reduce()

Now let's move on to implementing them.

Extending a Class

Classes in JavaScript can be extended using the extends keyword.

class ExtendedMap extends Map {   constructor() {}}
class ExtendedMap<K, V> extends Map<K, V> {   constructor() {}}

Since we are extending an existing class, we should follow up with a super() call.

class ExtendedMap extends Map {   constructor() {       super();   }}
class ExtendedMap<K, V> extends Map<K, V> {   constructor() {       super();   }}

Cool. Next comes adding custom methods.

Implementing Methods

class ExtendedMap extends Map {   constructor() {       super();   }   foo() {       console.log("bar")   }}
class ExtendedMap<K, V> extends Map<K, V> {   constructor() {       super();   }   foo() {       console.log("bar")   }}

You should now be able to access the extended map.

const myMap = new ExtendedMap();myMap.foo();
bar

Map.find()

The find() method performs a linear search on the collection and returns the first element that passes our condition.

For this, we first convert our Map into an iterator. To do so, we use Map.prototype.entries(). We can then iterate over the result using a for..of loop.

find() {   for (const [k, v] of this.entries()) {   }}
find(): V {   for (const [k, v] of this.entries()) {          }}

Our find() method needs one parameter, the test function. The function should return a truthy value in order to return an element.

find(fn) {   for (const [k, v] of this.entries()) {   }}
// Let's be a bit strict with the // test function's return type.find(fn: (v: V, k: K) => boolean): V {   for (const [k, v] of this.entries()) {          }}

Next is of course, running our test function on each entry in the map.

find(fn) {   for (const [k, v] of this.entries()) {       if(fn(v, k)) return v;   }}
find(fn: (v: V, k: K) => boolean): V {   for (const [k, v] of this.entries()) {       if(fn(v, k)) return v;   }}

There we have, our simple find() method. However, this method will return NOTHING if it didn't find a match. In that case, we can return a null or undefined.

find(fn) {   for (const [k, v] of this.entries()) {       if(fn(v, k)) return v;   }   return undefined;}
find(fn: (v: V, k: K) => boolean): V | undefined {   for (const [k, v] of this.entries()) {       if(fn(v, k)) return v;   }   return undefined;}

Map.filter()

The filter() method performs a linear search on the collection and returns the all elements that pass our condition.

The working is the same as our find() method.

filter(fn) {   for (const [k, v] of this.entries()) {   }}
filter(fn: (v: V, k: K) => boolean): V[] {   for (const [k, v] of this.entries()) {          }}

The only change is that we are returning an array of elements now.

filter(fn) {   const res = [];   for (const [k, v] of this.entries()) {       if(fn(v, k)) res.push(v);   }   return res;}
filter(fn: (v: V, k: K) => boolean): V[] {   const res = [];   for (const [k, v] of this.entries()) {       if(fn(v, k)) res.push(v);   }   return res;}

Unlike find(), method will return an empty array even if it didn't find a match. We don't need to explicitly return a fallback.

Map.map()

map() is a bit different. It is a method which runs a function on every element in the collection and returns an array of results.

Starting from where we left our filter,

map(fn) {   const res = [];   for (const [k, v] of this.entries()) {       if(fn(v, k)) res.push(v);   }   return res;}
map(fn: (v: V, k: K) => boolean): V[] {   const res = [];   for (const [k, v] of this.entries()) {       if(fn(v, k)) res.push(v);   }   return res;}

All we need to do is, remove the condition and push the function result instead.

map(fn) {   const res = [];   for (const [k, v] of this.entries()) {       res.push(fn(v, k));   }   return res;}
// Use the generic T for the function's result.map<T>(fn: (v: V, k: K) => T): T[] {   const res = [];   for (const [k, v] of this.entries()) {       res.push(fn(v, k));   }   return res;}

Easy, is it not? Now comes the final part of this guide.

Map.reduce()

The reduce() method accepts a reducer callback as a parameter and executes the callback on each element, with the previous iteration's result as a parameter. Like a chain.

Let's continue from where we left our map().

reduce(fn) {   const res = [];   for (const [k, v] of this.entries()) {       res.push(fn(v, k));   }   return res;}
// Use the generic T for the function's result.reduce<T>(fn: (v: V, k: K) => T): T[] {   const res = [];   for (const [k, v] of this.entries()) {       res.push(fn(v, k));   }   return res;}

We will need two parameters, one being the callback and the other being the initial value of our result.

reduce(fn, first) {   const res = first;   for (const [k, v] of this.entries()) {       res = fn(res, [k, v]);   }   return res;}
// Use the generic T for the function's result.reduce<T>(fn: (acc: T, val: [K, V]) => T, first): T {   const res = first;   for (const [k, v] of this.entries()) {       res = fn(res, [k, v]);   }   return res;}

And that's it! We have implemented reduce() for our extended Map.

Our result

Check out the full source code with even more methods at retraigo/bettermap

class ExtendedMap extends Map {   constructor() {       super();   }   find(fn) {       for (const [k, v] of this.entries()) {           if(fn(v, k)) return v;       }       return undefined;   }   filter(fn) {       const res = [];       for (const [k, v] of this.entries()) {           if(fn(v, k)) res.push(v);       }       return res;   }   map(fn) {       const res = [];       for (const [k, v] of this.entries()) {           res.push(fn(v, k));       }       return res;   }   reduce(fn, first) {       const res = first;       for (const [k, v] of this.entries()) {           res = fn(res, [k, v]);       }       return res;   }}
class ExtendedMap<K, V> extends Map<K, V> {   constructor() {       super();   }   find(fn: (v: V, k: K) => boolean): V | undefined {       for (const [k, v] of this.entries()) {           if(fn(v, k)) return v;       }       return undefined;   }   filter(fn: (v: V, k: K) => boolean): V[] {       const res = [];       for (const [k, v] of this.entries()) {           if(fn(v, k)) res.push(v);       }       return res;   }   map<T>(fn: (v: V, k: K) => T): T[] {       const res = [];       for (const [k, v] of this.entries()) {           res.push(fn(v, k));       }       return res;   }   reduce<T>(fn: (acc: T, val: [K, V]) => T, first): T {       const res = first;       for (const [k, v] of this.entries()) {           res = fn(res, [k, v]);       }       return res;   }}