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.
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
// Bad
const yyyymmdd = moment().format("YYYY/MM/DD");
// Better
const currentDate = moment().format("YYYY/MM/DD");
// 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…
// 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.
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.
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:
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;
for...in
for...of
and for await...of
loopsvar 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).
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);
catch
binding variabletry {
throw new TypeError("Something went wrong");
} catch ({ name, message }) {
console.log(name); // "TypeError"
console.log(message); // "Something went wrong"
}
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']
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}`;
// Bad
array.forEach((n) => { /.../ });
// Better
array.forEach((node) => { /.../ });
// Bad
function createBlock(name: string, lines?: number): string {
const linesAmount = lines || 8;
// logic
}
// Better
function createBlock(name: string, lines = 8): string {
// logic
}
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);
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)
}
}
// 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)) {
/.../
}
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.