paint-brush
Clean Code: Naming and Code Composition in TypeScript [Part 2]by@alenaananich
1,017 reads
1,017 reads

Clean Code: Naming and Code Composition in TypeScript [Part 2]

by Alena AnanichOctober 19th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Learn best practices for writing clear, efficient, and readable code that saves time and enhances developer productivity.
featured image - Clean Code: Naming and Code Composition in TypeScript [Part 2]
Alena Ananich HackerNoon profile picture


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

  1. Naming

  2. Destructuring assignment

    a. Binding pattern

    b. Assignment pattern

  3. Template literals for string concatenations

  4. Function parameters

  5. Optional changing

  6. Async await instead of a promise chain

  7. Conditionals


1. Naming


  1. Use meaningful and pronounceable names of variables
// Bad
const yyyymmdd = moment().format("YYYY/MM/DD");

// Better
const currentDate = moment().format("YYYY/MM/DD");


  1. 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…


  1. 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:

  1. 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;


  1. 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).


  1. 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);


  1. 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

  1. Avoid one-letter parameters even in callbacks.
// Bad
array.forEach((n) => { /.../ });

// Better
array.forEach((node) => {  /.../ });


  1. 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

  1. 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)) {
  // ...
}


  1. 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.