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 integral part of being a Software Engineer, just like estimations, brainstorming, writing unit tests are important aspects, writing clean code is really important as well.
In this article, we're going to look at 8 different techniques to help you write cleaner JavaScript code.
If you'd prefer watching a video instead, check this out:
Video Tutorial
Now let's discuss each technique, one at a time.
Pure Functions
A pure function is a function that always returns the same output, given the same input(s). It doesn't depend on any external variable apart from the inputs provided, nor it affects/changes any outside variable.
Having pure functions makes it a lot easier for testing as they make testing super easy as you can always stub/mock the inputs and test your expected values. Let's see the following example
let name = "Peter Parker";
const splitName = () => {
name = name.split(' ');
}
name = splitName();
console.log(name); // outputs [ 'Peter', 'Parker' ]
While the above code seems appropriate. It is not (lol). And that's because the splitName
function depends on an outside variable named name
and if someone else starts changing this variable, the function splitName
starts providing a different output. Making it a non-pure function as we'd still be calling splitName()
but the output is going to be different.
Let's change this to a pure function and let's see how that would look like:
let name = "Peter Parker";
const splitName = (nameString) => {
return nameString.split(' ');
}
name = splitName(name);
console.log(name); // outputs [ 'Peter', 'Parker' ]
With the above change, the splitName
is now a Pure Function because:
- It only relies on the input(s) (the
nameString
input). - It doesn't change/re-assign any external variable
Fewer or Named Parameters
When using functions, we often use positional parameters which have to be provided as they're declared with the function declaration. For example, in the call arithmaticOp(num1, num2, operator)
, we can't provide the operator
argument without providing num1
and num2
. And while this works for this example, for many functions, that'd become a problem.
Consider the following example:
const createButton = (title, color, disabled, padding, margin, border, shadow) => {
console.log(title, color, disabled, padding, margin, border, shadow)
}
Looking at the above code, you can already see that in if we wanted to make any of the arguments optional (to use default values) while calling the createButton
+, that'd be a disaster and might look something like this:
createButton('John Wick', undefined /* optional color */, true ,'2px....', undefined /* optional margin*/);
You can see that the above statement doesn't look Clean at all. Also, it is hard to see from the function-calling statement which parameter corresponds to which argument of the function. So this is a practice we could follow:
- If we have 2 or fewer arguments, we can keep them as positional arguments
- Else, we provide an object with key-value pairs
Let's use this technique with the above example and see how it looks like:
const createButton = ({title, color, disabled, padding, margin, border, shadow}) => {
console.log(title, color, disabled, padding, margin, border, shadow)
}
createButton({
title: 'John Wick',
disabled: true,
shadow: '2px....'
});
Notice that the statement to call the createButton
function is much cleaner now. And we can easily see which value in the key-value pair corresponds to the arguments for the functions. Yayy! 🎉
Object / Array Destructuring
Consider the following javascript example in which we're taking some properties from an object and assigning to their individual variables:
const user = {
name: 'Muhammad Ahsan',
email: 'hi@codewithahsan.dev',
designation: 'Software Architect',
loves: 'The Code With Ahsan Community'
}
const name = user.name;
const email = user.email;
const loves = user.loves;
In the above example, it is much cringe to use the user.*
notation so many times. This is where Object Destructuring comes in. We can change the above example as follows with Object Destructuring:
const user = {
name: 'Muhammad Ahsan',
email: 'hi@codewithahsan.dev',
designation: 'Software Architect',
loves: 'The Code With Ahsan Community'
}
const {name, email, loves} = user;
See! Much better. Right? Let's consider another example:
const getDetails = () => {
return ['Muhammad Ahsan', 'hi@codewithahsan.dev', 'Some Street', 'Some City', 'Some Zip', 'Some Country'];
}
const details = getDetails();
const uName = details[0];
const uEmail = details[1];
const uAddress = `${details[2]}, ${details[3]}, ${details[4]}, ${details[5]}`;
const uFirstName = uName.split(' ')[0];
const uLastName = uName.split(' ')[1];
Ugh. I even hated the code writing the above example 🤣. Had to do it though. You can see that the code looks super weird and is hard to read. We can use Array Destructuring to write it a bit cleaner as follows:
const getDetails = () => {
return ['Muhammad Ahsan', 'hi@codewithahsan.dev', 'Some Street', 'Some City', 'Some Zip', 'Some Country'];
}
const [uName, uEmail, ...uAddressArr] = getDetails();
const uAddress = uAddressArr.join(', ');
const [uFirstName, uLastName] = uName.split('');
console.log({
uFirstName,
uLastName,
uEmail,
uAddress
});
You can see how cleaner this is 🤩
Avoid Hard-coded values
This is an issue that I often request changes for the Pull Requests I review. And is a no-go. Let's see an example:
/**
* Some huge code
*
*
*
*
*
*/
setInterval(() => {
// do something
}, 86400000);
// WHAT IS THIS 86400000 ??? 🤔
Someone looking at the code would have no idea what this number stands for, how it was calculated and what's the business logic behind this. Instead of hardcoding this value, we could've created a constant as follows:
const DAY_IN_MILLISECONDS = 3600 * 24 * 1000; // 86400000
setInterval(() => {
// do something
}, DAY_IN_MILLISECONDS);
// now this makes sense
Let's consider another example:
const createUser = (name, designation, type) => {
console.log({name, designation, type});
}
createUser('Muhammad Ahsan', 'Software Architect', '1');
// WHAT IS this '1'? 🤔
Looking at the call for createUser
method. It is really hard for someone reading the code to understand what this '1'
stands for. I.e. what type
of user this is. So instead of hard-coding the value '1'
here, we could've created an Object mapping of the type of users we have as follows:
const USER_TYPES = {
REGULAR_EMPLOYEE: '1'
}
const createUser = (name, designation, type) => {
console.log({name, designation, type});
}
createUser('Muhammad Ahsan', 'Software Architect', USER_TYPES.REGULAR_EMPLOYEE);
// smoooooooth 😎
Avoid Short-hand variable names
Short-hand variables make sense where they're needed. Like if you've positional coordinates like x
and y
, that works. But if we create variables like p
, t
, c
without having a context, it is really hard to read, trace and maintain such code. See this example for instance:
const t = 25;
let users = ['Muhammad Ahsan', 'Darainn Mukarram'];
users = users.map((user) => {
/**
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
return {
...user,
tax: user.salary * t / 100 // WHAT IS `t` again? 🤔
}
})
The above examples shows that now the developer/reader has to scroll all the way up or go to the definition to try to understand what this variable is. Ergo NOT CLEAN CODE 😠. This is also called mind-mapping the variables in which only the author knows what they mean. So instead of the short hand variable name, we could've given this a proper name as follows:
const taxFactor = 25;
let users = ['Muhammad Ahsan', 'Darainn Mukarram'];
users = users.map((user) => {
// some code
return {
...user,
tax: user.salary * taxFactor / 100
}
})
And now this makes much more sense.
Set default Object values using Object.assign()
There might be cases where you'd want to create a new object from another object, providing some default value if the source object doesn't have them. Consider the following example:
const createButton = ({title, color, disabled, padding}) => {
const button = {};
button.color = color || '#333';
button.disabled = disabled || false;
button.title = title || '';
button.padding = padding || 0;
return button;
}
const buttonConfig = {
title: 'Click me!',
disabled: true
}
const newButton = createButton(buttonConfig);
console.log('newButton', newButton)
Instead of doing all that, we can use Object.assign()
to override the default properties if provided by the source object as follows:
const createButton = (config) => {
return {
...{
color: '#dcdcdc',
disabled: false,
title: '',
padding: 0
},
...config
};
}
const buttonConfig = {
title: 'Click me!',
disabled: true
}
const newButton = createButton(buttonConfig);
console.log('newButton', newButton)
Use method chaining (especially for classes)
Method chaining is a technique that can be useful if we know the user of the class/object is going to use multiple functions together. You might have seen this with libraries like moment.js. Let's see an example:
class Player {
constructor (name, score, position) {
this.position = position;
this.score = score;
this.name = name;
}
setName(name) {
this.name = name;
}
setPosition(position) {
this.position = position;
}
setScore(score) {
this.score = score;
}
}
const player = new Player();
player.setScore(0);
player.setName('Ahsan');
player..setPosition([2, 0]);
console.log(player)
In the above code, you can see that we needed to call a bunch of functions together for the player. If this is the case for your object/class, use method chaining. And all you need to do is to return the object's instance from the functions you want to chain. The above example can be modified as follows to achieve this:
class Player {
constructor (name, score, position) {
this.position = position;
this.score = score;
this.name = name;
}
setName(name) {
this.name = name;
return this; // <-- THIS
}
setPosition(position) {
this.position = position;
return this; // <-- THIS
}
setScore(score) {
this.score = score;
return this; // <-- THIS
}
}
const player = new Player();
player.setScore(0).setName('Ahsan').setPosition([2, 0]);
// SUPER COOL 😎
console.log(player)
Use Promises over Callbacks
Promises have made our lives easier. We had something called callback hell a couple of years ago that made the code so hard to read. It looks something like this:
Even if I'm working with a library that has callbacks, I try to add a wrapper there that promisifies that (yeah, that's a term now). Let's consider the following example:
const getSocials = (callback) => {
setTimeout(() => {
callback({socials: {youtube: 'youtube.com/CodeWithAhsan', twitter: '@codewith_ahsan'}});
}, 1500);
}
const getBooks = (callback) => {
setTimeout(() => {
callback({books: ['Angular Cookbook']});
}, 1500);
}
const getDesignation = (callback) => {
setTimeout(() => {
callback({designation: 'Software Architect'});
}, 1500);
}
const getUser = (callback) => {
setTimeout(() => {
callback({user: 'Ahsan'});
}, 1500);
}
getUser(({user}) => {
console.log('user retrieved', user)
getDesignation(({designation}) => {
console.log('designation retrieved', designation)
getBooks(({books}) => {
console.log('books retrieved', books)
getSocials(({socials}) => {
console.log('socials retrieved', socials)
})
})
})
})
All of the functions in the above code are asynchronous and they send back the data after 1.5 seconds. Now if there were 15 different functions involved, imagine what it would look like. Probably like the image I shared above 😅. Instead of having this callback hell, we can promisify our functions and use promises as follows for better readability:
const getSocials = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({socials: {youtube: 'youtube.com/CodeWithAhsan', twitter: '@codewith_ahsan'}});
}, 1500);
})
}
const getBooks = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({books: ['Angular Cookbook']});
}, 1500);
})
}
const getDesignation = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({designation: 'Software Architect'});
}, 1500);
})
}
const getUser = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({user: 'Ahsan'});
}, 1500);
})
}
getUser()
.then(({user}) => {
console.log('user retrieved', user);
return getDesignation();
})
.then(({designation}) => {
console.log('designation retrieved', designation)
return getBooks();
})
.then(({books}) => {
console.log('books retrieved', books);
return getSocials();
})
.then(({socials}) => {
console.log('socials retrieved', socials)
})
You can see that the code already is much readable now as all the .then()
statements are indented and show what data is retrieved in each .then()
step. We can easily see the steps using this syntax as every .then()
call returns the next function call along with its promise.
Now we can take it up a notch and make our code even more readable. How? By using async await
. We'll modify our code as follows to achieve that:
const getSocials = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({socials: {youtube: 'youtube.com/CodeWithAhsan', twitter: '@codewith_ahsan'}});
}, 1500);
})
}
const getBooks = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({books: ['Angular Cookbook']});
}, 1500);
})
}
const getDesignation = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({designation: 'Software Architect'});
}, 1500);
})
}
const getUser = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({user: 'Ahsan'});
}, 1500);
})
}
const performTasks = async () => {
const {user} = await getUser();
console.log('user retrieved', user);
const {designation} = await getDesignation();
console.log('designation retrieved', designation);
const {books} = await getBooks();
console.log('books retrieved', books);
const {socials} = await getSocials();
console.log('socials retrieved', socials);
}
Notice that we wrapped our code inside the performTasks()
function which is an async
function as you can see the usage of the async
keyword. And inside, we're making each function call using the await
keyword which essentially would wait for the promise from the function to be resolved before executing the next line of code. And with this syntax, our code look as if it was all synchronous, however being asynchronous. And our code is a lot more cleaner 🙂
Conclusion
I hope you enjoyed reading the article. If you did, make sure to hit like and bookmark. And check my YouTube Channel for more amazing content. And if you feel adventurous and are interested in taking your #Angular skills to the next level, check out my Angular Cookbook
Top comments (16)
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
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
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.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:
Oh. Yeah, definitely. This should most likely be in a constants file or something. You're right.
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:I personally would have written is like this:
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.. (:
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
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 way4 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
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
For default object values is there a reason you are spreading out the two objects. Is there an advantage over doing
or using the default values in the arguments?
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.@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.
Awesome Article, Thanks for the great techniques, you provided in this great article
Anytime!! Thanks.
Hey.. Remember update your Twitter in your Dev Profile: dev.to/codewithahsan
I don't see the value to use piped functions (even with pipe operator) ? It's less readable and requires more code and characters.