DEV Community

Cover image for 5 JavaScript "tips" that might bite you back.
Igor Snitkin
Igor Snitkin

Posted on

5 JavaScript "tips" that might bite you back.

How many times have you seen articles saying "Do not do that", "This is wrong" or "These tips will make you a pro developer" etc. ๐Ÿ˜‹ I don't know about you, but I've seen enough of those. Don't get me wrong, many of the tips are actually useful and quite valuable, it's not a problem with the implementation itself, rather it's a problem with imitation aka copy/pasting.

Tip meme

Let's see and discuss a couple of those tips. Before we start though, let's determine types of bites, as they would differ by impact:

  • Readability Bite: will not affect you directly, it will rather bite your teammate who reviews your code.
  • Type Bite: will bite with using certain types
  • Syntax Bite: will bite using certain syntactic expression

Alright! Without further ado.

1. Convert to number

This is my favorite, and I have to admit I myself use it all the time. The trick is quite simple, by attaching Unary Plus(+) operator to any value you will force it to be coerced to number:


const strNum = '3645';
const strNaN = 'hi, i am not a number';

typeof +strNum; // "number"
typeof +strNaN; // "number"

+strNum; // 3645
+strNaN; // NaN

Enter fullscreen mode Exit fullscreen mode

This tip is quite light on errors and works pretty much all the time, it's a suggested conversion method by many teams.

Readability Bite:

I'm pretty sure, you saw it coming ๐Ÿ™‚ It's no-brainer that any developer who doesn't know how unary plus operator works will WTF following code:


function sum(a, b) {
  return +a + +b;
}

Enter fullscreen mode Exit fullscreen mode

Not mentioning the fact that we're all fans of functional programming and this ๐Ÿ‘† doesn't align very well with its principles.

Type Bite:

Unfortunately this will not work with BigInt, a new numeric data type introduced in 2019.


const veryBigInt = 45n;
+veryBigInt; // TypeError: Cannot convert a BigInt value to a number

Enter fullscreen mode Exit fullscreen mode

Before you start complaining in the comments below, I'm pretty aware that your app will never deal with this type, but we all can agree that functionality that makes no presumptions is more stable.

Solution:

One solution that improves readability, is functional and accounts for BigInt:


const veryBigInt = 45n;
const strNum = '3645';
const strNaN = 'hi, i am not a number';

Number(veryBigInt); // 45
Number(strNum); // 3645
Number(strNaN); // NaN

Enter fullscreen mode Exit fullscreen mode

I'm not including conversion to a string here, as from readability point of view it bites the same way:


const ugly = 42 + '';
const good = String(42);
const goodToo = `${42}`;

Enter fullscreen mode Exit fullscreen mode

2. Concatenate arrays

Another extremely popular tip โ€” concatenate arrays using Spread Operator:


const a = [1, 2, 3];
const b = [4, 5, 6];

[...a, ...b]; // [1, 2, 3, 4, 5, 6]

Enter fullscreen mode Exit fullscreen mode

How on Earth this might bite? Well let's say I kinda like this functionality and I want to extract it into function (because functional programming, you know ๐Ÿค—).

Type Bite:

So here's our union function:


function union(a, b) {
  return [...a, ...b];
}

Enter fullscreen mode Exit fullscreen mode

I have one issue right from the start - I want union of any number of arrays, not just two. Any ideas how to refactor it still using the spread operator?

The second issue is that it will include empty cells which depending on situation might not be desirable:


const a = [1, 2, 3];
const b = Array(3);
b.push(4);
union(a, b); // [1, 2, 3, undefined, undefined, undefined, 4]

Enter fullscreen mode Exit fullscreen mode

Finally, we would need to be really really careful with what we're passing as arguments to union:


const a = [1, 2, 3];
const b = null;
const c = 42;
const d = 'hello';

union(a, b); // TypeError: b is not iterable
union(a, c); // TypeError: c is not iterable
union(a, d); // [1, 2, 3, "h", "e", "l", "l", "o"] :/

Enter fullscreen mode Exit fullscreen mode

Putting union aside, this approach forces you to always presume values are arrays, which is pretty bold assumption.

Solution:

Let's rewrite our function, so it accounts for all the issues above:


function union(...args) {
  return args.flat();
}

const a = [1, 2, 3];
const b = null;
const c = 42;
const d = 'hello';
const e = Array(3);
e.push(99);

union(a, b, c, d, e); // [1, 2, 3, null, 42, "hello", 99]

Enter fullscreen mode Exit fullscreen mode

I think I hear CS maniacs screaming at me now "Flat iiiss sloooow!" Ok. If your program operates with arrays over 10000 items and you worry about performance, then use .concat():


function union(...args) {
  return [].concat(...args);
}

Enter fullscreen mode Exit fullscreen mode

A bit more performant way, but grabs empty cells. Chances that you will deal with empty cells are super tiny anyway ๐Ÿ‘

I guess my message here is that the .concat() method is not obsolete and you shall not treat it this way. Furthermore using functions over operators will make your program just a little bit more stable.

3. Round number using bitwise operators.

Low-level nature of bitwise operators makes them VERY fast and on top of that you have to admit they are quite nerdy and I see how many people can be attracted to them ๐Ÿค“. Of course any bitwise operator will cause Readability Bite, we won't even discuss it.

Let's get back to "rounding". You might notice that different people will do it with different operators, popular ones are bitwise OR | and double bitwise NOT ~~. In fact you can use all of them:


const third = 33.33;
/* Bitwise AND */
third & -1; // 33

/* Bitwise NOT */
~~third; // 33

/* Bitwise OR */
third | 0; // 33

/* Bitwise XOR */
third ^ 0; // 33

/* Left shift */
third << 0; // 33

/* Right shift */
third >> 0; // 33

/* Zero fill right shift (positive numbers only) */
third >>> 0; // 33

Enter fullscreen mode Exit fullscreen mode

What's going on?!! Too good to be true, isn't it? Well, yes. You are not "rounding" anything you just using bitwise operators to return the same number here and given the fact that bitwise operators can only operate on 32-bit integers this effectively truncates float numbers, because they are not in 32-bit range. Which brings us...

Syntax Bite

32-bit integers are integers ranging from -2,147,483,648 to +2,147,483,647. That might sound like a lot, but in fact it's probably the average video count of Justin Bieber on YouTube. As you might guess this won't work outside the range:


const averageBieberViewsCount = 2147483648.475;
averageBieberViewsCount | 0; // -2147483648 ๐Ÿฅฒ
~~averageBieberViewsCount; // -2147483648 ๐Ÿฅฒ

Enter fullscreen mode Exit fullscreen mode

On top of this, it's not rounding in the first place, rather truncating off the fractional part of the number:


const almostOne = 0.9999999;
almostOne | 0; // 0 :/

Enter fullscreen mode Exit fullscreen mode

And finally, this approach has strange relationship with NaN which can cause pretty nasty bugs:


~~NaN; // 0

Enter fullscreen mode Exit fullscreen mode

Solution

Just use function built for this:


const third = 33.33;
const averageBieberViewsCount = 2147483648.475;
const almostOne = 0.9999999;

Math.round(third); // 33
Math.round(averageBieberViewsCount); // 2147483648
Math.round(almostOne); // 1
Math.round(NaN); // NaN

Enter fullscreen mode Exit fullscreen mode

4. Rounding with Number.toFixed

While we're on the topic of rounding, let's see one more that is quite popular, especially when dealing with any sort of currency-related numbers:


const number = 100 / 3;
const amount = number.toFixed(2); // "33.33"

Enter fullscreen mode Exit fullscreen mode

Floating numbers in any programming language is a problem, unfortunately it is true for JavaScript and .toFixed() is no exception.

Syntax Bite

The problem occurs in the rounding edge case when last digit to be rounded is 5. By rounding rules such case should be rounded up, so:


(1.5).toFixed(0); // 2 ๐Ÿ‘
(1.25).toFixed(1); // 1.3 ๐Ÿ‘
(1.725).toFixed(2); // 1.73 ๐Ÿ‘
/* and so on */

Enter fullscreen mode Exit fullscreen mode

Unfortunately it's not always a case:


(0.15).toFixed(1); // 0.1 ๐Ÿ‘Ž
(6.55).toFixed(1); // 6.5 ๐Ÿ‘Ž
(1.605).toFixed(2); // 1.60 ๐Ÿ‘Ž

Enter fullscreen mode Exit fullscreen mode

As you can see we're not talking about rounding to extreme precisions here, rounding to one or two decimal places is normal everyday routine.

Solution

One of the solutions is to use third-party rounding to precision function, like _.round() or similar. Or just write your own such function, it's not a rocket science ๐Ÿš€:


function round(number, precision = 0) {
  const factor = 10 ** precision;
  const product = Math.round(number * factor * 10) / 10;
  return Math.round(product) / factor;
}

round(0.15, 1); // 0.2 ๐Ÿ‘
round(6.55, 1); // 6.6 ๐Ÿ‘
round(1.605, 2); // 1.61 ๐Ÿ‘

Enter fullscreen mode Exit fullscreen mode

Cool by-product of such function is that you have negative precision rounding aka number of trailing zeros right off the bat:


round(12345, -3); // 12000
round(12345, -2); // 12300
round(12345, -1); // 12350
round(-2025, -1); // -2020

Enter fullscreen mode Exit fullscreen mode

5. Higher-order methods "shortcuts"

Another very popular trick is to use pre-built functions as arguments to higher-order methods (methods that expect function as an argument), it works exceptionally good with .map() and .filter():


const randomStuff = [5, null, false, -3, '65'];

/* Convert to string */
randomStuff.map(String); // ["5", "null", "false", "-3", "65"]

/* Convert to number */
randomStuff.map(Number); // [5, 0, 0, -3, 65]

/* Filter out falsy values */
randomStuff.filter(Boolean); // [5, -3, "65"]

/* Falsy check */
!randomStuff.every(Boolean); // true

Enter fullscreen mode Exit fullscreen mode

You get the point... Super hacky, super cool ๐Ÿ˜Ž

Syntax Bite

Let's say I need to parse some CSS margin value, pretty reasonable task:


const margin = '12px 15px';
const parsedMargin = margin.split(/\s+/).map(parseInt);

console.log(parsedMargin); // [12, NaN] :/

Enter fullscreen mode Exit fullscreen mode

Every high-order method will invoke a given function passing 3 arguments: element, index, reference to original array. What's happening is on every iteration of method parseInt function is given at least two arguments, and that's exactly how many arguments parseInt expects: string to parse and radix, so we're ending up passing index of the element as radix:


/* Iteration #1 */
parseInt('12px', 0); // Radix 0 is ignored and we get 12

/* Iteration #2 */
parseInt('15px', 1); // Radix 1 doesn't exists and we get NaN

Enter fullscreen mode Exit fullscreen mode

Solution

You can always check how many arguments the function you want to use expects using .length, if it's more than 1 then it is probably unsafe to pass this function as an argument and instead we'll need to wrap it:


parseInt.length; // 2

const parsedMargin = margin
  .split(/\s+/)
  .map((margin) => parseInt(margin));

console.log(parsedMargin); // [12, 15] ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰

Enter fullscreen mode Exit fullscreen mode

Conclusion

Ask yourself a hmmm..

Don't just blindly follow whatever is written online, question yourself, research, test, then double research and double test. "It just works" should never be an excuse! If you don't know why it works, then presume it doesn't.

I actually prepared 10 tips for this article, but it appeared to be too long and code heavy for one post, so I might do a follow up soon unless you completely destroy me in the comments. Speaking of comments, feel free to discuss and let me know if you experienced any tips and tricks that have bitten you in the past.

Happy New 2021!

Top comments (32)

Collapse
 
brandondamue profile image
Brandon Damue

I like that part where you say "if you don't know why it works, then presume it doesn't". Great write up waiting for the follow up post.

Collapse
 
harshrathod50 profile image
Harsh Rathod

Yeah, it is indeed a piece of great advice. Usually, I too, don't believe what is written until I'd myself seen or done practically.

Collapse
 
pris_stratton profile image
pris stratton

Great read. Point 1 is also my favourite, โ€œ+โ€ over โ€œNumber()โ€ just makes no sense as a choice to me. The latter is ridiculously clear, the former is just ridiculous.

Collapse
 
thepeoplesbourgeois profile image
Josh

I want union of any number of arrays, not just two. Any ideas how to refactor it still using the spread operator?

function union(a, ...b) {
  b.reduce((b, a) => [...a, ...b], a)
}
Enter fullscreen mode Exit fullscreen mode

Granted, the spread operator being used in the returned array like this means you'll be generating copies upon copies upon copies, but you asked for an arity-agnostic refactor, not a performant one ๐Ÿ˜œ

Collapse
 
eecolor profile image
EECOLOR

Note that your ordering is a bit weird. You could do this (swap b and a in the reduce):

function union(...arrays) {
  return arrays.reduce((result, x) => [...result, ...x], [])
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
thepeoplesbourgeois profile image
Josh

Every single time I use reduce, I forget how whichever language I'm using it in organizes either its own arguments, or the arguments given to its lambda/block. ๐Ÿคฆโ€โ™‚๏ธ

Ruby:

init = 0
(1..10).reduce(init) do |accumulator, iteration| 
  accumulator + iteration  
end # 55
Enter fullscreen mode Exit fullscreen mode

Elixir:

init = 0
Enum.reduce(1..10, init, fn iteration, accumulator ->
  accumulator + iteration
end) # 55
Enter fullscreen mode Exit fullscreen mode

And now, (thank you,) Javascript:

function union(a, ...b) {
  return b.reduce((accumulator, iteration) => [...accumulator, ...iteration], a)
}
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
eecolor profile image
EECOLOR

Haha, yeah. In Scala you have foldLeft and foldRight, where foldLeft has it as the first argument and foldRight as the second argument.

Collapse
 
paulsmithkc profile image
Paul Smith

This instantiates n+1 new arrays (where n is the number of arrays), when you could have just used a for loop to instantiate 1 new array.

Thread Thread
 
eecolor profile image
EECOLOR

Did you read the comment of @josh to which I replied?

Granted, the spread operator being used in the returned array like this means you'll be generating copies upon copies upon copies, but you asked for an arity-agnostic refactor, not a performant one ๐Ÿ˜œ

Collapse
 
chrdek profile image
chrdek

Syntax bite # 5 for '12px 15px' can be also rewritten as:
margin.split(/px/g).map(Number) or margin.split('px').map(Number).
Produces a result of [12,15,0]. Still usable in case you only need to do basic calculations (add, subtract).

Collapse
 
paulsmithkc profile image
Paul Smith

You're still assuming all of the values have a unit and that unit is pixels.

This doesn't work for zero.

Collapse
 
chrdek profile image
chrdek

Great, thanks for pointing that out. Aside from that, it is an alternative for the specific example using integer casting.

Thread Thread
 
paulsmithkc profile image
Paul Smith

You missed the whole point of this article. Which that you should not be writing code that way in the first place. Nor should you be suggesting this buggy code to anybody else.

Collapse
 
dvddpl profile image
Davide de Paolis

Don't just blindly follow whatever is written online, question yourself, research, test, then double research and double test. "It just works" should never be an excuse! If you don't know why it works, then presume it doesn't.

absolutely love this and agree 100%
this would just require a post on its own.

it summarize the right positive investigative attitude to good coding practice and learning in general.

Collapse
 
mrwensveen profile image
Matthijs Wensveen

A worthy article to read. Thanks!
My two cents:

  1. Why would you even create a 'sum' function when there is an operator that does just that? FP does not mean that you have to make functions for everything and your grandmother.
  2. Again, FP does not mean you have to make functions where expressions are readily available. The function actually hides that you're using spread, possibly surprising the callers of the function with unexpected results. They're better off using the appropriate method of concatenation where it is needed.
  3. Whoever says you can use bitwise operators to "round" is just plain lying. But it is an extremely fast way to cast your number to int (in other languages cast to int also truncates).
  4. I have to say I was surprised by this. Good tip!
  5. The main point to take away from this tip is that you don't have to use lambda's all over the place, which a lot of developers do, but that you can supply a function that takes the appropriate number of arguments. BTW, not every higher-order method takes 3 arguments, by definition. Just the ones on Array :)
Collapse
 
waynevanson profile image
Wayne Van Son

My favourite part was "solution: use a function". How great is functional programming!

Great abstraction with the "bites", a great article to read :)

Collapse
 
lucassperez profile image
Lucas Perez

What's not love about
variable + ''
1+ +a
!!thing
if (!value)

Collapse
 
khorne07 profile image
Khorne07

Find some tricks very helpfull, specially points 4 & 5 ๐Ÿ–’. But in point one I prefer using + operator to make conversions to number, is faster to type and is you hardly manage big ints in your app so the unary + operator works perfect. Also I like to use !! to convert to boolean type. Just a matter of personal taste ๐Ÿ˜. Very good article ๐Ÿ–’. I'll be waiting gor the 2nd part.

Collapse
 
alexleung profile image
Alex Leung

Seems like using TypeScript solves most of these