## DEV Community is a community of 616,766 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

# Why you need the tap method

Amin Updated on ・6 min read

Let's say you have a script that process an array with multiple transformations to finally compute your result.

``````"use strict";

const input = "732829320";

const valid =
Array
.from(input)
.map(character => parseInt(input) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;

console.log(valid);
``````

As you can see, we are using a lot of Array methods to ease the development of our script that could have been really verbose if written without these methods. But still, there is a problem and impossible to know what is happening since I'm really tired and it is late night. The `valid` variable stores the `false` value, even if our input is valid.

By the way, I am here validating a SIREN number, which is a special enterprise identification number used in France to identify companies. You don't need to understand what is going on here but for those who are curious, it uses the Luhn algorithm, which is the same algorithm used to validate VISA's credit card numbers.

Maybe you tried something like that.

``````"use strict";

const input = "732829320";

const valid =
Array
.from(input)
.forEach(character => console.log(character))
.map(character => parseInt(input) || 0)
// Error: cannot read property map of undefined
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;

console.log(valid);
``````

WARNING: this is not production-ready code. Don't copy/paste it to solve this problem in your applications! It is just a pretext to give you an example of how the `tap` method can be used here.

Unfortunately, it won't work due to the fact that the `forEach` method will return undefined, meaning that it cannot be chained by others calls to `map`, `filter`, `reduce`, etc...

But we could store values for each steps and just log the output of each. That's a solution.

``````"use strict";

const input = "732829320";

const array = Array.from(input);

console.log(array);

const digits = array.map(character => parseInt(input) || 0);

console.log(digits);

const multiplied = digits.map((digit, index) => index % 2 === 0 ? digit : digit * 2)

console.log(multiplied);

const digitSum = multiplied.map(digit => digit > 9 ? digit - 9 : digit);

console.log(digitSum);

const sum = digitSum.reduce((sum, digit) => sum + digit);

console.log(sum);

const valid = sum % 10 === 0;

console.log(valid);
``````

But it is really verbose, like a lot. And I had to come up with new names for my variables, which is something I wasted time since I will not use these except for the purpose of logging them.

But it works, and I finally managed to figure why I had an error. The second log for the `digits` variable gives me something like that:

``````[ 732829320,
732829320,
732829320,
732829320,
732829320,
732829320,
732829320,
732829320,
732829320 ]
``````

Which is weird at first glance since I was expecting to turn all my characters to a single digit. But in reality, I am parsing the `input` variable, instead of the `character` variable. So here is my error. I found it and successfully validated my script.

``````"use strict";

const input = "732829320";

const valid =
Array
.from(input)
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;

console.log(valid);
``````

But can we do better? Yes! By using a `tap` method. In a nutshell, and in this case, a `tap` method will help you loop through your array, without touching it, and will return it to be chained in others calls. If you didn't understand, that's okay. An example is worth a hundred words.

``````"use strict";

const input = "732829320";

const valid =
Array
.from(input)
.tap(character => console.log(character))
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;

console.log(valid);
``````

As you can see, we are using the `tap` method to log our characters, before they can be mapped to numbers in the next `map` call. All I did was to branch my `tap` method between those calls and tada, we got a logging of our data without even having to make a mess in our code. The `tap` method here will produce the following output.

``````7
3
2
8
2
9
3
2
0
``````

And we can keep going and branch our `tap` method as much as we want since by definition, it will always return the same thing, meaning an array of data.

Let's be crazy and branch it everywhere.

``````"use strict";

const input = "732829320";

const valid =
Array
.from(input)
.tap(character => console.log(character))
.map(character => parseInt(character) || 0)
.tap(character => console.log(character))
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.tap(character => console.log(character))
.map(digit => digit > 9 ? digit - 9 : digit)
.tap(character => console.log(character))
.reduce((sum, digit) => sum + digit) % 10 === 0;

console.log(valid);
``````

Of course, this will log out many things, maybe not the best way to debug our code but it is an example of how far you could go with this method. And of course, you could shorten this call by passing `console.log` as a first-class function.

``````"use strict";

const input = "732829320";

const valid =
Array
.from(input)
.tap(console.log)
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;

console.log(valid);
``````

Or do anything else with it! But remember that it will always return the array untouched, so even if you try to update the array, this won't return the updated values to the next chained call!

Ok, ok... I will now show you how to implement this so called `tap` method. First of all, we need to augment the capabilities of the `Array` object in JavaScript to be able to chain call the `tap` method like that.

``````Array.prototype.tap = function() {
// ...
};
``````

Now, we need to find a way to get the array that we want to iterate over. We can do that by using the `this` keyword to get the full array. Let's use a `for...of` loop to loop over each elements of that array.

``````Array.prototype.tap = function() {
for (const element of this) {
// ...
}
};
``````

Now we need to do something... As you can see, in the previous examples, we passed a function as a first-class citizen. So looks like we are getting a callback as our parameter. Let's use this callback by passing it the current iterated element of our array.

``````Array.prototype.tap = function(callback) {
for (const element of this) {
callback(element);
}
};
``````

Last thing we want to do in order to prevent breaking the chain of calls we made earlier is to return the array untouched. Since the `for...of` loop won't update the array here, we can safely return the `this` keyword that will reference the original array here.

``````Array.prototype.tap = function(callback) {
for (const element of this) {
callback(element);
}

return this;
};
``````

But nothing tells us that the people behind the ECMAScript standard won't implement a `tap` method as part of the `Array` prototype. Maybe they will read this article and think about how useful this function is! And if you keep your script as is, and use a newer (hypothetic) version of JavaScript that implement such a functionality, you may end up breaking your script as this definition will clash with the standard definition. We need to add a special guard to prevent such cases from happening.

``````if (!Array.prototype.tap) {
Array.prototype.tap = function(callback) {
for (const element of this) {
callback(element);
}

return this;
};
}
``````

Ah! That's better. We could also make the `for...of` loop a one liner using the `forEach` method of Arrays instead. Since `this` is an array, it can easily be used for this purpose, just for the sake of saving some bytes.

``````if (!Array.prototype.tap) {
Array.prototype.tap = function(callback) {
this.forEach(element => callback(element));

return this;
};
}
``````

And here is the final source-code.

``````"use strict";

if (!Array.prototype.tap) {
Array.prototype.tap = function(callback) {
this.forEach(element => callback(element));

return this;
};
}

const input = "732829320";

const valid =
Array
.from(input)
.tap(console.log)
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;

console.log(valid);
``````

Now you can easily track down your state and bugs by using this neat little trick!

You could also use a `map` to mimic this kind of behavior, without having to write a definition for the `tap` method.

``````const valid =
Array
.from(input)
.map(character => { console.log(character); return character; })
.map(character => parseInt(character) || 0)
.map((digit, index) => index % 2 === 0 ? digit : digit * 2)
.map(digit => digit > 9 ? digit - 9 : digit)
.reduce((sum, digit) => sum + digit) % 10 === 0;
``````

And it would totally work! It has the advantage of not taking the risk of clashing with an hypothetical ECMAScript definition of a `tap` method (though we added a guard for such case) and with the drawback of being a little bit of a mouthful.

Should you use it? Some are saying that using prototype-based inheritance in some cases can lead to problematic behavior that can be hard to track in case of bugs. But I think we can agree that well used, these kinds of patterns can be powerful and really enjoyable to use for the Developer Experience. There is an interesting conversation in the comment section that continues on that idea so I suggest you don't stop just here and keep going!

## Discussion (6)

Ben Calder

Interesting... but you need a warning that this is an anti-pattern:

``````Array.prototype.tap = function() {
// ...
};
``````

You should almost always avoid adding to built-in prototypes!

I also may have misunderstood the problem; but why not do everything in a single reduce?

``````const input = "732829320";

const valid =
input.split('') // old-school conversion of string to array :)
.reduce((sum, character, index) => {
console.log(character);
// assuming you're confident you're getting numerical values
const digit = index % 2 === 0 ? +character : +character * 2
return sum + (digit > 9 ? digit - 9 : digit);
}) % 10 === 0;

console.log(valid);
``````

If nothing else you can achieve the equivalent of your `tap` method with map:

``````.map(character => {
console.log(character);
return character;
})
``````
Amin

Interesting... but you need a warning that this is an anti-pattern:
You should almost always avoid adding to built-in prototypes!

Maybe I didn't quite catch all that was in your example but could you elaborate more on how is this an anti-pattern? I think it can greatly benefit to others reading your comment!

From my personal point of view, I don't think it is an anti-pattern as it is a feature of the language that, well used, can be really useful, powerful and cool to use! And by well used, I mean no overriding of existing prototypes, just augmenting the capabilities of the language as I said in this post. I also said that maybe they (the TC39 committee's people) will add a `tap` method but looking at what the proposals are here, I think we are good for a while since it is not even a staged feature (and probably never will, but I can dream, right? haha).

I also may have misunderstood the problem; but why not do everything in a single reduce?

That's a really great way of reducing the complexity of the problem. That clearly makes it shorter. But maybe harder to work on.

Unfortunately, that was not the point of this article as this was only a pretext to show how to use the `tap` prototype we built in this post. But I'll keep in mind your idea as it may serves me someday! Thanks for pointing that out. I'll update my post accordingly to explain clearly that this is not a production code.

If nothing else you can achieve the equivalent of your tap method with map:

You are correct! That's an another way of doing the tap method as an equivalent. But isn't it a bit of a mouthful though? Adding the `tap` method with the `console.log` call just between two `map` calls seems to me shorter, easier to work with especially when doing only debugging. If I ever forgot to put the last return at the end, I'll end up with two bugs (the one that I am looking for and this one). Plus it forces you to write two separate instructions as the `console.log` call does not return the called arguments (unfortunately).

I guess what I am trying to say here is that JavaScript is a wonderful piece of programming language and that it supports many programming paradigms. It's up to the reader to choose how he want to use this language. I'm just trying to show other opportunities of doing things but it absolutely does not mean that it's the best way of doing it. Maybe it won't suit you and that's fine to me.

Again, thank you for your comment. I'm taking notes of what you said to improve my article!

Ben Calder • Edited

The reason for not extending built-in objects is well documented and founded on historical experience. It's a similar reason for avoiding global variables: you risk naming clashes and unexpectedly breaking code elsewhere in your application.

I'm not saying the approach is entirely without merit; but I think it's sensible to include a warning about this up-front so people understand the risks ;)

You do (later) add a guard; but you only talk about 'tap' being added to the language. But the problems come when you're working with multiple libraries; each of which extends the same native object with a same named function; each with a different implementation... Welcome to bug hell :(
Extending native prototypes happened a lot in the early days of JS; and people soon realised it was a really bad idea.

Understood on this being example code; and agreed that the solution does boil down to coding style ;)
The suggestion to use map() with a console log was included to show that there was a simple enough alternative to tap that didn't come with the risk of extending the native prototype.

And my comment wasn't meant as a criticism: it's a good article; and if nothing else highlights JavaScript's often misunderstood use of prototype-based inheritance.

Amin

Thanks for clearing that out!

I'm divided between showcasing a feature, but knowing this is a risky one that can be misused or not sharing something that could be useful for others. But in the end, I always end up sharing with the community! That's why I try to add as many guards as possible and experiment a little bit on my own. As shown in this article.

Agreed on saying that using this pattern in a library and sharing that with the OSS community can lead to several and critical bugs, that's why I personally never do that (mostly because I'm only working in a private community and rarely write entire modules for the OSS community, unfortunately!).

But this is a subject I would love to talk about for hours and would greatly deserve an entire post...!

Dor Shinar

Thanks for the article. I don't fully understand the difference between your `Array#tap` and the built-in `Array#forEach`.

Amin • Edited

Hey Dor, thanks for your message!

The simple difference is that the`forEach` method will not return anything except `undefined`. This is a problem! Since we want to map to a function but still continue the chain calls of `map`s. So we cannot do something like that in JavaScript.

``````
[1, 2, 3].map(something).forEach(console.log).map(somethingElse);
``````

Since it will return `undefined`, you will get an error trying to chain call the `map` method on an `undefined` value.

I'll add that to my post because I think it can benefit to others. Thanks again for your question!