DEV Community

Cover image for 8 techniques to write cleaner JavaScript code

8 techniques to write cleaner JavaScript code

Muhammad Ahsan Ayaz on January 03, 2022

I believe being a Software Engineer is just like being a Super Hero! And with great power, comes great responsibility. While writing code is an int...
Collapse
 
loucyx profile image
Lou Cyx

Nice article, a few things to add:

  • You can make this one even better:
const splitName = nameString => {
    return nameString.split(" ");
};
Enter fullscreen mode Exit fullscreen mode

We just need to get rid of the block we are not using, and we can also make it curried so is more reusable. And instead of calling it splitName, we just call it what it is: spaceSplit:

const split = separator => string => string.split(separator);
const spaceSplit = split(" ");

spaceSplit("Peter Parker"); // ["Peter", "Parker"]
Enter fullscreen mode Exit fullscreen mode
  • You can use Object.assign when you want to actually update a newly created object that's more complex than a plain one, take this example:
const createElement = tagName => props =>
    Object.assign(document.createElement(tagName), props);
const createButton = createElement("button");

createButton({
    className: "btn",
    textContent: "Hello there!",
}); // <button class="btn">Hello there!</button>
Enter fullscreen mode Exit fullscreen mode
  • Instead of using chained methods, you could just pipe functions:
const set = property => value => object => ({
    ...object,
    [property]: value,
});

const setName = set("name");
const setPosition = set("position");
const setScore = set("score");

const pipe = functions => value =>
    functions.reduce((output, pipedFunction) => pipedFunction(output), value);

const player = pipe([setScore(0), setName("Ahsan"), setPosition([2, 0])])({});
Enter fullscreen mode Exit fullscreen mode

With some luck we'll get the pipe operator soon, and this will be even cleaner:

const player = setScore(0)({}) |> setName("Ahsan")(%) |> setPosition([2, 0])(%);
Enter fullscreen mode Exit fullscreen mode
  • Finally, the async/await solution can be also simplified without the need for async/await:
const delayed = timeout => value => () =>
    new Promise(resolve => setTimeout(() => resolve(value), timeout));

const delayed1500 = delayed(1500);

const getSocials = delayed1500({
    socials: {
        youtube: "youtube.com/CodeWithAhsan",
        twitter: "@codewith_ahsan",
    },
});
const getBooks = delayed1500({ books: ["Angular Cookbook"] });
const getDesignation = delayed1500({ designation: "Software Architect" });
const getUser = delayed1500({ user: "Ahsan" });

const log = prepend => log => console.log(prepend, log);

getUser()
    .then(log("user retrieved"))
    .then(getDesignation)
    .then(log("designation retrieved"))
    .then(getBooks)
    .then(log("books retrieved"))
    .then(getSocials)
    .then(log("socials retrieved"));
Enter fullscreen mode Exit fullscreen mode

Cheers!

Collapse
 
romeerez profile image
Roman K

The article was about general advises, and you are suggesting a functional programming approach which is worth a separate article :)

The approach is tricky, leads to unclean code (what createElement does? creates a function!) so I wouldn't recommend it as a general advise

Collapse
 
loucyx profile image
Lou Cyx

You and me seem to have pretty different definitions of "unclean" .... you might want to take a look at currying ... and "unclean" libraries such as ramda 😅

Thread Thread
 
romeerez profile image
Roman K • Edited

Guess what isQueenOfSpades contains in ramda docs? Guessing not works in here, in such way need to read source to know. While in normal JS where naming matters it is a boolean.

Is it clean? In common programming sense, name is given to describe what function or variable does, so in this way no, it's not clean. Functional programming has roots in math where naming doesn't matter, they are cool with formulas like this one, but it's not right to expect your teammates will be happy of guessing how your log works.

Thread Thread
 
loucyx profile image
Lou Cyx • Edited

Let's go one thing at a time:

  1. You don't need to read the source to understand a function, nowadays we have inline docs and typing systems.
  2. Is it really hard for you to figure out what isQueenOfSpades does? Do you prefer the name to be isAnObjectWhichContainsARankQAndASuitOfSpades instead?
  3. A name should hint what a function does, but you don't need to explain the entire implementation in the name.
  4. If you're working with currying, you already know every function is an unary that returns a new function until you have all the arguments.
  5. Programming has roots in binary and asm, and that doesn't have anything to do with the way we do things nowadays. I would never advocate for naming practices like the ones used in languages like Haskell, with single letters. Using Haskell to debate FP practices is like using ASM to justify that programming is hard to understand.
  6. Nobody needs to "guess" how something works if it has good docs, types and a clear enough name.

Do you use libraries without knowing what the utils they provide will return, and relying only on the name? I imagine back in the day you had a really hard time with jQuery's $.get if that's the case. My teammates are perfectly fine with a log function, because when they use it they get autocompletion in their editors telling them the arguments that function takes, the thing it returns, and if they use it wrong they get a red squiggly underline to let them know, not to mention that if they hover over the function or the arguments, they get examples from the JSDocs 😄

Thread Thread
 
romeerez profile image
Roman K • Edited

Agree, makes sense now, I get used to rely on naming and I'm not hovering things at all, while in this practice programmer has to rely on hovering hint. I've tested and it works nicely, even in plain JS I got hint

log = (prepend: any) => (log: any) => void
Enter fullscreen mode Exit fullscreen mode

Libraries is a different thing and only way is it read docs and memoize frequently used stuff.

$.get - jQuery was intended to do what document.querySelector does before it appeared, so I expect it to get element by selector. I honestly didn't check docs. But if it was ramda I would expect: get(key, object) and get(key)(object), but wrong, it's a 'prop' in ramda.

1 nowadays we have inline docs and typing systems.

So in general thanks for response, in editors it's not hard to get hints.

But not everywhere, while reviewing code in github it can give you hints as well, but far less intelligent and it won't work so nicely.

2 and 3 isAnObjectWhichContainsARankQAndASuitOfSpades

I would have a type Card = { rank: string, suite: string }

And a function getIsCardAQueenOfSpaces.

And now I can assign it to local variable isQueenOfSpaces and use it in imperative way

4 Makes sense, but it means whole project need to follow FP and currying to stay predictable, everyone in team must be good with FP and be on same page

Do you use libraries without knowing what the utils they provide will return, and relying only on the name?

Yes, sure, other teammates may introduce libs which I never used and I'm reviewing the code, and usually it's understandable just by looking on name and usage

Collapse
 
codewithahsan profile image
Muhammad Ahsan Ayaz

@lukeshiru You're right. Thanks for the tips. I tried to keep things in this article (and the video). But yeah, once we get the pipe operator, things are going to be much more fun.

Collapse
 
regislutter profile image
Régis LUTTER

I don't see the value to use piped functions (even with pipe operator) ? It's less readable and requires more code and characters.

Collapse
 
loucyx profile image
Lou Cyx

Have you actually tried currying? Let's use split as an example to compare different approaches:

// This block are just the values we'll use in our examples, you can ignore them

const spaced1 = "hello world";
const spaced2 = "goodbye world";
const dotted1 = "hello.world";
const dotted2 = "goodbye.world";
const spacedArray = [...Array(10)].fill(spaced1);
const dottedArray = [...Array(10)].fill(dotted2);

/* Without helper functions ***************************************************/

const splitSpaced1 = spaced1.split(" ");
const splitSpaced2 = spaced2.split(" ");
const splitDotted1 = dotted1.split(".");
const splitDotted2 = dotted2.split(".");
const splitSpacedArray = spacedArray.map(spaced => spaced.split(" "));
const splitDottedArray = dottedArray.map(dotted => dotted.split(" "));

/* With helper functions with no options **************************************/

// More code here:
const spaceSplit = string => string.split(" ");
const dotSplit = string => string.split(".");

// But way less code and more readability here:
const splitSpaced1 = spaceSplit(spaced1);
const splitSpaced2 = spaceSplit(spaced2);
const splitDotted1 = dotSplit(dotted1);
const splitDotted2 = dotSplit(dotted2);
const splitSpacedArray = spacedArray.map(spaceSplit);
const splitDottedArray = dottedArray.map(dotSplit);

/* With helper functions with options *****************************************/

// A little bit more code here:
const split = (string, separator) => string.split(separator);

// A little bit more readability and reuse here:
const spaceSplit = string => split(string, " ");
const dotSplit = string => split(split, ".");

// Usage is the same as above, so same benefits

/* With curried functions *****************************************************/

// Same amount of characters as above here:
const split = separator => string => string.split(separator);

// But way less code here, and even more readable:
const spaceSplit = split(" ");
const dotSplit = split(".");

// Usage is almost the same as above, with extra benefits, we'll see that next
Enter fullscreen mode Exit fullscreen mode

You can see you how you only write "more" if you use it once, but the power of currying comes from reuse, and that's where it becomes more readable and where you end up writing more. Keeping the examples above, let's say a new requirement appears in which some strings might come with the _ character, here's how we would solve that with each approach:

const snakeCased = "hello_world";
const snakeCasesArray = [...Array(10)].fill(snakeCased);

/* Without utils **************************************************************/

const splitSnakeCasedArray = snakeCasesArray.map(snakeCased =>
    snakeCased.split("_"),
);

/* With utils without options *************************************************/

// Again like before, we wrote more code (it could be less)
const snakeCaseSplit = string => string.split("_");

// But more readability and less code when using
const splitSnakeCasedArray = snakeCasesArray.map(snakeCaseSplit);

/* With utils with options ****************************************************/

// A little bit of reuse here
const snakeCaseSplit = string => split("_", string);

// Usage is the same as above

/* Finally, with currying *****************************************************/

// Way less code, and still readable
const snakeCaseSplit = split("_");

// And usage can be the same as above, but also if is only once place you could:
const splitSnakeCasedArray = snakeCasesArray.map(split("_"));
Enter fullscreen mode Exit fullscreen mode

I understand that every time we do something thinking in reuse it will require a little bit more characters first, but then it requires less everywhere else. In general readability is improved because we go from stuff like this:

array.map(item => item.split(" "));
Enter fullscreen mode Exit fullscreen mode

To stuff like this:

array.map(split(" "));
Enter fullscreen mode Exit fullscreen mode

Not to mention that map itself can be curried like this:

const map = mapper => array => array.map(mapper);
Enter fullscreen mode Exit fullscreen mode

And then you get even more reuse and readability:

const spaceSplit = split(" ");
const spaceSplitMap = map(spaceSplit);

spaceSplitMap(array);

// And the "inline" version for single case uses:

map(split(" "))(array);
Enter fullscreen mode Exit fullscreen mode

Currying is amazing for code size and reuse, and is very readable if we stay away from "Haskell like" practices like having single character arguments. Hope this helps see the value of currying more clearly. I was requested a few times to do a full post on the subject and I will do it soon, so I'll try to remember to let you know about it if you still don't see the value in this.

Cheers!

Collapse
 
minhan1910 profile image
minhan1910

I'm seeing a useful of pipe operator now. Thank Luke Shiru.

Collapse
 
samantrags profile image
Raghavendra Samant

Great writeup Muhammud .

Q: const DAY_IN_MILLISECONDS = 3600 * 24 * 1000; // 86400000
isnt it better to put the breakup in comments to avoid the calculations being done every time ? Performance over ease of understanding

Collapse
 
codewithahsan profile image
Muhammad Ahsan Ayaz

Thanks Raghav,
I didn't understand the question. With const DAY_IN_MILLISECONDS = 3600 * 24 * 1000; we're performing the calculations one time and that constant can be used multiple times now. And we can definitely have a better comment around it.

Collapse
 
luishrodg profile image
Luis Rodrigues

I believe that what Raghav meant was, if you put this variable in an kind of component that calculation will be done every time that i use the component.

use this:

const DAY_IN_MILLISECONDS = 86400000 // 3600 * 24 * 1000
Enter fullscreen mode Exit fullscreen mode
Thread Thread
 
codewithahsan profile image
Muhammad Ahsan Ayaz

Oh. Yeah, definitely. This should most likely be in a constants file or something. You're right.

Collapse
 
zalithka profile image
Andre Greeff

I'm normally somewhat sceptical of posts in this category, simply because so many of these points end up being very closely tied to the wonderful concept of "personal preference".. that said, this has some real gems, with some good clear examples. nicely done!

with regards to the idea of avoiding short-hand variable names, I agree wholeheartedly with what you're saying, but (..and there it is) I have one small exception to this "rule": and that is dinky, tiny, one-liner helper functions..

using your first splitName function as an example, simply because of how small it actually is, instead of writing it like this:

const splitName = (nameString) => {
  return nameString.split(' ');
}
Enter fullscreen mode Exit fullscreen mode

I personally would have written is like this:

const splitName = (n) => n.split(' ');
Enter fullscreen mode Exit fullscreen mode

with that said, as soon as the function arguments start growing, or the function body requires more than a single line, then both the implicit return and short-hand variable names go straight out the window.. tied to a rock.. shot by a cannon.. sometimes without even opening the window first.. (:

Collapse
 
robertrynard profile image
Robert-Rynard

For default object values is there a reason you are spreading out the two objects. Is there an advantage over doing

const createButton = (config) => {
    return {
      color: "#dcdcdc",
      disabled: false,
      title: "",
      padding: 0,
      ...config
    };
}
Enter fullscreen mode Exit fullscreen mode

or using the default values in the arguments?

const createButton = ({ title = "", color = "#333", disabled = false, padding = 0 }) => {
  return {
      title,
      color,
      disabled,
      padding
  };
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rafiqm profile image
Muhammad Rafiq

Awesome Article, Thanks for the great techniques, you provided in this great article

Collapse
 
codewithahsan profile image
Muhammad Ahsan Ayaz

Anytime!! Thanks.

Collapse
 
gabrielizalo profile image
Gabriel Porras

Hey.. Remember update your Twitter in your Dev Profile: dev.to/codewithahsan

Collapse
 
xc profile image
xavier-kong

Great article! "Fewer or Named Parameters" and "Avoid Hard-coded values" are the ones that really stood out to me the most!