In Part 1 of this series of articles, we looked at more common mistakes in functions and methods composition. In this article, we will focus on writing code that will be readable by a human-like book. We will consider best practices in naming, code composition, and code optimization. These practices will prevent developers from wasting time in debugging and maintaining code.
Table of contents
-
Naming
-
Destructuring assignment
a. Binding pattern
b. Assignment pattern
-
Template literals for string concatenations
-
Function parameters
-
Optional changing
-
Async await
instead of a promise chain -
Conditionals
1. Naming
- Use meaningful and pronounceable names of variables
// Bad
const yyyymmdd = moment().format("YYYY/MM/DD");
// Better
const currentDate = moment().format("YYYY/MM/DD");
- Use the same style naming for the same type of variables or functions
// Bad
getProductInfo();
getFoodData();
setGroceriesUpdate();
// Better
getProductInfo();
getProductData();
setProductUpdate();
In this example, we have a set of functions working with the same logical level of data. To focus on this, we need to follow one naming pattern get/setProduct…
- Use searchable names
// Bad
buildHeadlinesList(documents: Doc[], 5) {/.../}
// Better
const HEADLINES_COUNT = 5;
buildHeadlinesList(documents, HEADLINES_COUNT);
In this example, we have a magic number 5
in arguments. But for clear code, we need to create a constant with a meaningful name. It will give an understanding of what we are doing in function.
2. Destructuring assignment
The destructuring assignment syntax makes it possible to assign values from arrays or properties from objects into distinct variables. With this approach, we make our code compact and reduce needless computations.
There are two patterns in destructuring assignments: binding pattern and assignment pattern. Let’s take a look at both of them.
a. Binding pattern
The binding destructuring pattern starts with the declaration keyword var, let, const
. Then, each individual property must either be bound to a variable or further destructured.
Binding patterns could be realized in the following ways:
- Simple variable assignment
const product = { name: "Lemon", count: 200, country: "Portugal" };
// Bad
const name = product.name;
const count = product.count;
const country = product.country;
// Better
const { name, count, country } = product;
- Looping variable inÂ
for...in
Âfor...of
andÂfor await...of
 loops
var users = [
{ user: "Alex", bio: { age: 35, country: "Poland" }, phone: "700-12-13" },
{ user: "Mike", bio: { age: 21, country: "France" } },
];
for (let { user, phone = "DEFAULT VALUE", bio: { age, country } } of users) {
console.log(user, phone, age, country);
}
In this example, we are destructuring an object with nested destructuring in for of
loop to top up all values on one level and define "DEFAULT VALUE"
if the key is missed in the target object or has undefined
value (if it is null
default value will not be assigned).
- Function parameters
interface Product {
name: string,
count: number,
country: string,
}
const product: Product = { name: "Lemon", count: 200, country: "Portugal" };
// Bad
function analyzeProducts(product: Product): void {
const name = product.name;
const count = product.count;
const country = product.country;
// ...
}
// Better
function analyzeProducts(product: Product): void {
const { name, count, country } = product;
// ...
}
analyzeProducts(product);
- TheÂ
catch
 binding variable
try {
throw new TypeError("Something went wrong");
} catch ({ name, message }) {
console.log(name); // "TypeError"
console.log(message); // "Something went wrong"
}
b. Assignment pattern
In the assignment pattern, there are no keywords var, let, const
. Each deconstructed property gets assigned to a target in the assignment. This target can be pre-declared using var, let, const
, or it can be a property of another object — essentially, anything permissible on the left side of an assignment expression.
const users = [];
const objUsers = { a: "Alex", b: "Mark" };
({ a: users[0], b: users[1] } = objUsers); // ['Alex', 'Mark']
3. Template literals for string concatenations
The template literals offer brevity, cleanliness, and support for multiline strings.
// Bad
const helloMessage = “Hi ” + name + “, today is “ + currentDay;
// Better
const helloMessage = `Hi ${name}, today is ${currentDay}`;
4. Function parameters
- Avoid one-letter parameters even in callbacks.
// Bad
array.forEach((n) => { /.../ });
// Better
array.forEach((node) => { /.../ });
- Use default parameters instead of short-circuiting or conditionals
// Bad
function createBlock(name: string, lines?: number): string {
const linesAmount = lines || 8;
// logic
}
// Better
function createBlock(name: string, lines = 8): string {
// logic
}
5. Optional changing
In cases when we need to merge some objects, update some fields, or leave the old ones if they were not updated, it will be good practice to use spread
operator or Object.assign()
instead of conditions.
const user = {
name: 'Alex',
email: "[email protected]",
organisation: "XXX",
secondPhone: 56459544,
country: "Poland"
}
const updatedUserData = {
email: "[email protected]",
organisation: "ZZZ",
}
// Bad
const updatedUser = {
name: updatedUserData.name || user.name,
email: updatedUserData.name || user.email,
organisation: updatedUserData.organisation || user.organisation,
secondPhone: updatedUserData.secondPhone || user.secondPhone,
country: updatedUserData.country || user.country
}
// Better, 1 option
const updatedUser = { ...user, ...updatedUserData };
// Better, 2 option
const updatedUser = Object.assign(user, updatedUserData);
6. Async await instead of the promise chain
When you have a set of chained promises, it would be a good practice to use async await
to make code readable.
// Bad
analyzeProducts(products: string[]): Promise<AnalyzedProduct[]> {
return this.productService.analyze(this.buildQueryParam(products))
.then((products: Product[]) => {
requestIds(products)
.then((products: AnalyzedProduct[]) => mapProducts(products));
.catch((error: ) => {
notifyUser(error)
})
})
}
// Better
async analyzeProducts(products: string[]): Promise<AnalyzedProduct[]> {
try {
const analyzedProducts = await this.productService.analyze(this.buildQueryParam(products));
const productsWithIds = await this.productService.requestIds(products);
return mapProducts(productsWithIds));
} catch(error) {
notifyUser(error)
}
}
7. Conditionals
- Encapsulate conditionals
// Bad
if (token.category === Token.Operator && isFirstToken && !token.disabled) {
// ...
}
// Better
function showToken(token: Token, model: Model<Token>): boolean {
return (token.category === Token.Operator && model.length && !token.disabled)
}
if (showToken(token, model)) {
// ...
}
-
Avoid negative conditionals
For readability, it is recommended to use affirmative conditions rather than negative ones.
// Bad
if (!tokenIsNotHidden(node)) {
/.../
}
// Better
if (tokenIsHidden(node)) {
/.../
}
Conclusion
In this article, we considered the best practices for code readability. In the next article, we will take a look at code smells in Classes, Objects, and Data Structures and the ways it could improved.