Extending the JavaScript Map
Tags: javascript
Published: 2023-04-11 09:14:03

> This is meant to provide an understanding of how stuff work. This is not the "best" method to do any of the mentioned stuff.

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.

```ts
 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.

```ts
 class ExtendedMap extends Map {
    constructor() {}
 }
```

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

```ts
 class ExtendedMap extends Map {
    constructor() {
        super();
    }
 }
```

Cool. Next comes adding custom methods.

## Implementing Methods

```ts
 class ExtendedMap extends Map {
    constructor() {
        super();
    }
    foo() {
        console.log("bar")
    }
 }
```

You should now be able to access the extended map.

```ts
const myMap = new ExtendedMap();
myMap.foo();
```

```stdout
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.

```ts
 find() {
    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.

```ts
 find(fn) {
    for (const [k, v] of this.entries()) {

    }
 }
```

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

```ts
 find(fn) {
    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`.


```ts
 find(fn) {
    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 all elements that pass our condition.

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


```ts
 filter(fn) {
    for (const [k, v] of this.entries()) {

    }
 }
```

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


```ts
 filter(fn) {
    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`,


```ts
 map(fn) {
    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.


```ts
 map(fn) {
    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()`.

```ts
 reduce(fn) {
    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.

```ts
 reduce(fn, first) {
    let 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](https://github.com/retraigo/bettermap).


```ts
 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;
    }
 }
```