DEV Community

Cover image for The Elegance of Clean Code: A Fundamental Guide with JavaScript
Alvison Hunter Arnuero | Front-End Web Developer
Alvison Hunter Arnuero | Front-End Web Developer

Posted on • Updated on

The Elegance of Clean Code: A Fundamental Guide with JavaScript

In the world of JavaScript development, there is a secret recipe for success that not everyone is aware of: writing clean, concise, and precise code. Clean code is like a well-organized kitchen, where every utensil has its place, concise code is the minimalist menu that gets straight to the point, and precise code is the chef's signature dish, executed with utmost accuracy.

In this article, we'll explore the importance of these principles and provide 10 JavaScript with TypeScript code snippets that exemplify clean code practices.


1. Meaningful Variable Names

One of the fundamental aspects of clean code is using meaningful variable names. Avoid cryptic names like "x" or "temp." Instead, opt for descriptive names that convey the purpose of the variable.

// Not so clean
const x = 42;

// Clean and clear
const age = 42;
Enter fullscreen mode Exit fullscreen mode

2. Consistent Formatting

Consistency in formatting is key to readability. Stick to a single style guide and be consistent with it. For TypeScript, use the TypeScript-specific formatting rules. Here are examples demonstrating the importance of consistency in formatting:

// Inconsistent formatting
function initGreeting(name) {
  return 'Good Morning, ' + name;
}

// Consistent formatting with TypeScript-specific rules
function initGreetingTSRules(name: string = "Joe"): string {
  return `Buenos dias, ${name}`;
}

// Anonymous Function
const initGreetingAnonymFn = (name : string = "Fred"): string => `Guten Morgen, ${name}`;

// Call these functions to display the greetings
console.log(initGreeting("Alvison"));
console.log(initGreetingTSRules());
console.log(initGreetingAnonymFn("Liam"));
Enter fullscreen mode Exit fullscreen mode

In the examples above, notice how the consistent use of formatting rules, such as type annotations and semicolons, improves the readability and maintainability of the code. Adhering to a consistent style guide not only makes the code look cleaner but also helps in avoiding potential errors and misunderstandings in the future.


3. Avoid Nesting Callbacks

Callback hell, also known as the "Pyramid of Doom," can make your code hard to follow and maintain. To keep your code clean and maintainable, use Promises, async/await, or other async control flow mechanisms.

Example of Callback Hell:

getUser((user) => {
  getProfile(user, (profile) => {
    displayProfile(profile);
  });
});
Enter fullscreen mode Exit fullscreen mode

In the above example, nested callbacks make the code harder to read and manage, especially as the complexity grows.

Cleaner with promises:

getUser()
  .then(getProfile)
  .then(displayProfile)
  .catch((error) => {
    console.error(error);
  });
Enter fullscreen mode Exit fullscreen mode

By using Promises, we can flatten the structure and make the flow of the code more apparent. Each .then method returns a new Promise, allowing us to chain further .then calls.

Even Cleaner with async/await:

async function showUserProfile() {
  try {
    const user = await getUser();
    const profile = await getProfile(user);
    displayProfile(profile);
  } catch (error) {
    console.error(error);
  }
}

showUserProfile();
Enter fullscreen mode Exit fullscreen mode

Using async/await makes the code even more readable by allowing us to write asynchronous code that looks synchronous. The try/catch block is used to handle errors gracefully.


4. Comment Sparingly

Comments are essential for explaining complex logic, but strive to write self-explanatory code. If your code is clear and concise, it will often speak for itself.

const arrSales: number[] = [10, 20, 30, 40, 50, 60, 70];

const calculateTotal = (sales: number[]): number => {
  return sales.reduce((acc: number, currentValue: number) => acc + currentValue, 0);
};

// Commented code
// const total = calculateTotal(arrSales); // Calculate the total price

// Clean code
const total: number = calculateTotal(arrSales);
console.log("Total sum:", total);

Enter fullscreen mode Exit fullscreen mode

In this snippet:

  • The arrSales array is defined at the beginning to ensure it's available for the calculateTotal function.
  • The calculateTotal function now takes an argument, sales, making it more flexible and reusable.
  • The commented code is an example of what you should avoid if the function name and logic are clear.
  • The clean code example shows the usage of the calculateTotal function and outputs the total sum to the console.

By making sure your function names and logic are clear, you can reduce the need for excessive comments, resulting in cleaner and more maintainable code.


5. Use Destructuring

Destructuring assignments can simplify your code and make it more readable, especially when working with objects and arrays. This technique allows you to extract values from objects and arrays and assign them to variables in a clean and concise way.

Example with an Object
Let's consider a simple example with a FrontEndWebDeveloper object:

interface FrontEndWebDeveloper {
  fullName: string;
  age: number;
}

const developer: FrontEndWebDeveloper = {
  fullName: "Alvison Hunter",
  age: 48,
};

// Without destructuring
const fullName: string = developer.fullName;
const age: number = developer.age;

console.log(`Name: ${fullName}, Age: ${age}`);

// With destructuring and alias to avoid duplicated keys
const { fullName: devName, age: devAge }: { fullName: string; age: number } = developer;

console.log(`Dev Name: ${devName}, Dev Age: ${devAge}`);
Enter fullscreen mode Exit fullscreen mode

Explanation:

  1. Without Destructuring:
const fullName: string = developer.fullName;
const age: number = developer.age;

console.log(`Name: ${fullName}, Age: ${age}`);
Enter fullscreen mode Exit fullscreen mode

This approach extracts values from the developer object and assigns them to individual variables. While straightforward, it can become cumbersome when dealing with multiple properties.

  1. With Destructuring:
const { fullName: devName, age: devAge }: { fullName: string; age: number } = developer;

console.log(`Dev Name: ${devName}, Dev Age: ${devAge}`);
Enter fullscreen mode Exit fullscreen mode

This approach simplifies the code by extracting properties in a single statement. Additionally, aliasing (fullName: devName, age: devAge) is used to avoid naming conflicts and improve readability.

Using destructuring not only reduces the amount of code but also makes it clearer and more maintainable. This is particularly beneficial when dealing with complex objects or multiple properties, enhancing the elegance and cleanliness of your code.


6. Avoid Magic Numbers

Magic numbers are hard-coded numerical values that lack context, making your code harder to read and maintain. Replace them with named constants or variables to improve readability and make your code more self-documenting.

Example of Magic Number

// Magic number
const status = 3;
if (status === 3) {
  console.log(`CURRENT STATUS: ${status}`);
}
Enter fullscreen mode Exit fullscreen mode

In this example, the number 3 is a magic number. It's not immediately clear what 3 represents, which can lead to confusion and errors.

Improved Code with Named Constant

// Named constant
const STATUS_COMPLETED = 3;
const status = STATUS_COMPLETED;

if (status === STATUS_COMPLETED) {
  console.log(`CURRENT STATUS: ${status}`);
}
Enter fullscreen mode Exit fullscreen mode

By using the named constant STATUS_COMPLETED, it's clear that 3 represents a specific status. This makes the code easier to understand and maintain.

Remember, avoiding magic numbers is a key practice in writing elegant, clean code. It not only makes your code more readable but also helps prevent errors when the same value needs to be used in multiple places.


7. Single Responsibility Principle

Functions should have a single responsibility. Split complex functions into smaller, focused ones.

type Order = {
  id: number;
  products: string[];
};

const orderLogMessages = {
  orderPayment: "Processing payment for order",
  orderUpdate: "Updating inventory for order",
  orderDelivery: "Delivering order"
}

// Complex function
const processOrder = async (order: Order): Promise<void> => {
  processPayment(order);
  updateInventory(order);
  dispatchOrder(order);
}

// Clean and concise
const processPayment = (order: Order): void => {
  console.log(`${orderLogMessages.orderPayment} ${order.id}`);
}

const updateInventory = (order: Order): void => {
  console.log(`${orderLogMessages.orderUpdate} ${order.id}`);
}

const dispatchOrder = (order: Order): void => {
  console.log(`${orderLogMessages.orderDelivery} ${order.id} with products: ${order.products.join(", ")}`);
}

// Let's invoke the function now
processOrder({ id: 2340, products: ["Fried Chicken", "Bread", "Ketchup"] });
Enter fullscreen mode Exit fullscreen mode

Explanation
In this example, the processOrder function was initially a complex function handling multiple tasks: processing payments, updating inventory, and dispatching orders. By following the Single Responsibility Principle, we split this function into three smaller, focused functions:

  1. processPayment: Handles the payment processing logic.
  2. updateInventory: Manages inventory updates.
  3. dispatchOrder: Takes care of dispatching the order.

Each function now has a single responsibility, making the code easier to understand, test, and maintain. The processOrder function coordinates these smaller functions, ensuring that the order processing workflow remains clear and concise.


8. Error Handling

Proper error handling is crucial. Always catch and handle errors gracefully rather than letting them crash your application.

Poor Error Handling
Inadequate error handling can leave your application vulnerable to unexpected crashes and can make debugging difficult. For example:

try {
  // call your fetch or any other operation here
} catch (e) {
  // Handle all errors
}
Enter fullscreen mode Exit fullscreen mode

In this example, errors are caught but not handled properly, leading to a generic handling of all errors without providing meaningful feedback or taking appropriate actions.

Precise Error Handling Using Async/Await
Using async and await allows for more precise error handling, providing a clear and maintainable approach. Here’s an improved version:

interface JokeResponse {
  id: number;
  type: string;
  setup: string;
  punchline: string;
}

async function fetchRandomProgrammingJoke() {
  try {
    const response = await fetch('https://official-joke-api.appspot.com/jokes/programming/random');

    if (!response.ok) {
      throw new Error(`Failed to fetch data. Status: ${response.status}`);
    }

    const data: JokeResponse[] = await response.json();

    if (data.length === 0) {
      // Handle an empty response
      throw new Error('No jokes found');
    }

    const joke = data[0];
    console.log(`Setup: ${joke.setup}`);
    console.log(`Punchline: ${joke.punchline}`);
  } catch (specificError) {
    // Handle errors gracefully
    console.error('An error occurred:', specificError);
  }
}

// Call the function to fetch and display the joke
fetchRandomProgrammingJoke();
Enter fullscreen mode Exit fullscreen mode

In this enhanced example:

Error Detection: The code checks if the response is not OK and throws an error with a specific message, including the status code.
Response Validation: The code ensures that the fetched data is not empty and throws an error if no jokes are found.
Error Reporting: Errors are logged to the console with a descriptive message, making it easier to debug and understand the issue.
This approach not only handles errors gracefully but also ensures that your application can provide meaningful feedback to users and developers, leading to more robust and maintainable code.


9. Avoid Unnecessary Complexity

Simplicity is the key to clean code. Avoid over-engineering or introducing unnecessary complexity.

Here's a comparison of an overly complex solution versus a simplified one:

const x: number = 5;
const y: number = 10;

// Overly complex
const complexResult: number = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));

// Simplified
const simplifiedResult: number = Math.hypot(x, y);
console.log(`Complex Result: ${complexResult}`);
console.log(`Simplified Result: ${simplifiedResult}`);
Enter fullscreen mode Exit fullscreen mode

In this example, the first solution uses Math.sqrt and Math.pow to calculate the hypotenuse of a right-angled triangle, which is unnecessarily complex. The simplified version uses Math.hypot, a built-in function that achieves the same result more concisely and readably.


10. TypeScript for Type Safety

TypeScript is a powerful tool for writing clean, concise, and precise JavaScript. It helps catch type-related errors early, making your code more robust and maintainable. By adding type annotations, TypeScript enhances code readability and provides a form of self-documentation. This is incredibly helpful for teams and projects of all sizes.

// Without TypeScript
function add(a, b) {
  return a + b;
}

// Usage example
console.log(add(2, 3)); // 5
console.log(add('2', '3')); // '23' (unexpected behavior)
Enter fullscreen mode Exit fullscreen mode
// With TypeScript
const addWithTypeScript = (a: number, b: number): number => {
  return a + b;
}

// Usage example
console.log(addWithTypeScript(2, 3)); // 5
console.log(addWithTypeScript('2', '3')); 
// Error: Argument of type 'string' is not assignable 
// to parameter of type 'number'
Enter fullscreen mode Exit fullscreen mode

By using TypeScript, you can avoid unexpected behavior and ensure your functions behave as intended. Type annotations act as a safeguard, catching potential errors at compile-time rather than at runtime. This leads to cleaner, more reliable code and a smoother development process.

Wrapping up:
In conclusion, clean, concise, and precise code is the hallmark of a skilled JavaScript developer. By following these principles and practicing good coding habits, you can create code that is not only easier to read and maintain but also less prone to bugs and errors. Start implementing these practices in your projects today and watch your codebase become a masterpiece of clarity and efficiency. Happy coding!


❤️ Enjoyed the article? Your feedback fuels more content.
💬 Share your thoughts in a comment.
🔖 No time to read now? Well, Bookmark for later.
🔗 If it helped, pass it on, dude!

Top comments (0)