Skip to content

Latest commit

 

History

History
848 lines (639 loc) · 16.5 KB

JAVASCRIPT_BEST_PRACTICES.md

File metadata and controls

848 lines (639 loc) · 16.5 KB

JavaScript Best Practices

This document outlines the best practices for writing JavaScript code. Each practice is demonstrated with a "do" and "don't" example to show the preferred conventions and common pitfalls to avoid.

Table of Contents

Async/Await

Do:

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

Don't:

function fetchData() {
  fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => {
      console.log(data);
    })
    .catch(error => {
      console.error('Error fetching data:', error);
    });
}

Object Property Access

Do:

const person = { name: 'Alice', age: 25 };
console.log(person.name);

Don't:

const person = { name: 'Alice', age: 25 };
console.log(person['name']);

Array Iteration

Do:

const numbers = [1, 2, 3, 4, 5];
const squares = numbers.map(number => number * number);

Don't:

const numbers = [1, 2, 3, 4, 5];
const squares = [];
for (let i = 0; i < numbers.length; i++) {
  squares.push(numbers[i] * numbers[i]);
}

Asynchronous Iteration

Do:

async function processArray(array) {
  for (const item of array) {
    await processItem(item);
  }
}

Do:

With concurrent processing

async function processArray(array) {
    await Promise.all(array.map(item) => await processItem(item))
}

Don't:

async function processArray(array) {
  await array.forEach(async (item) => { // <-- This will immediately return
    await processItem(item);
  });
}

Variable Declarations

Do:

const name = 'Alice'; // for variables that won't change.
let score = 5; // for variables that may change.

Don't:

var name = 'Alice'; // 'var' is function-scoped and can lead to confusion.

Template Literals

Do:

const greeting = `Hello, ${name}!`;

Don't:

const greeting = 'Hello, ' + name + '!';

Destructuring Assignment

Do:

const { name, age } = person;

Don't:

const name = person.name;
const age = person.age;

Spread and Rest Operators

Do:

const newArray = [...oldArray, newItem];
const { x, y, ...rest } = obj;

Don't:

const newArray = oldArray.concat(newItem);
const x = obj.x, y = obj.y;

Arrow Functions

Do:

const add = (a, b) => a + b;

Don't:

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

Modules

Do:

// es6 module syntax
import myFunction from './myModule';

Don't:

// CommonJS syntax
const myFunction = require('./myModule');

Default Parameters

Do:

function greet(name = 'Guest') {
  console.log(`Hello, ${name}!`);
}

Don't:

function greet(name) {
  name = name || 'Guest';
  console.log(`Hello, ${name}!`);
}

Error Handling

Do:

try {
  // code that may throw an error
} catch (error) {
  // error handling
}

Don't:

// code that may throw an error
// No error handling

Pure Functions

Do:

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

Don't:

let result;
function add(a, b) {
  result = a + b;
}

Callback Hell

Do:

async function asyncFlow() {
  const resultA = await asyncTaskA();
  const resultB = await asyncTaskB(resultA);
  return finalTask(resultB);
}

Don't:

function callbackHell() {
  asyncTaskA(function(resultA) {
    asyncTaskB(resultA, function(resultB) {
      finalTask(resultB);
    });
  });
}

Input Validation

Do:

function processInput(input) {
  if (typeof input !== 'string') {
    throw new TypeError('Input must be a string');
  }
  // process input
}

Don't:

function processInput(input) {
  // No validation, assuming input is always correct
  // process input
}

Shallow Copy

Do:

// Use the spread operator or `Object.assign` for creating a shallow copy of an object when nested objects are not a concern.
const original = { a: 1, b: 2 };
const shallowCopy = { ...original };

Don't:

// Use a shallow copy if your object contains nested structures and you need a deep copy, as changes to nested data will affect the original.
const original = { a: 1, b: { c: 3 } };
const shallowCopy = { ...original };
shallowCopy.b.c = 4; // This will change original.b.c to 4 as well.

Deep Copy

Do:

// Use a utility function like JSON.parse(JSON.stringify(object)) for a quick deep copy when you don't have functions or circular references in your object.
const original = { a: 1, b: { c: 3 } };
const deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.b.c = 4; // This will not affect original.b.c

Don't:

// Rely on JSON.parse(JSON.stringify(object)) for deep copying objects containing functions, dates, undefined, or circular references, as it will not preserve those values.
const original = { a: 1, b: () => console.log("Not copied!"), c: new Date() };
const deepCopy = JSON.parse(JSON.stringify(original));
// deepCopy.b is undefined, and deepCopy.c is a string, not a Date object.

For complex objects, consider using a utility library like Lodash's _.cloneDeep method, or write a custom deep copy function that handles all edge cases specific to your needs.

General Best Practices

Code Splitting

Do:

// Using dynamic imports for code splitting in a React application
const MyComponent = React.lazy(() => import('./MyComponent'));

Don't:

// Loading everything in a single large bundle
import MyComponent from './MyComponent';

Linting and Formatting

Do:

// Use ESLint and Prettier in your project for consistent coding styles
// Configure them according to your coding style preferences

Don't:

// Write code without any linting or formatting tools, leading to inconsistent styles

Testing

Do:

// Write unit tests for your functions
describe('add function', () => {
  it('adds two numbers', () => {
    expect(add(2, 3)).toBe(5);
  });
});

Don't:

// No testing
function add(a, b) {
  return a + b;
}

Comments and Documentation

Do:

/**
 * Adds two numbers together.
 * @param {number} a The first number.
 * @param {number} b The second number.
 * @returns {number} The sum of the two numbers.
 */
function add(a, b) {
  return a + b;
}

Don't:

// Adds two numbers
function add(a, b) {
  return a + b;
}

Refactoring

Do:

// Regularly revisit and refactor your code to improve clarity and efficiency

Don't:

// Leave old, unused, or inefficient code in the codebase without attempting to improve it

Security Practices

Do:

// Use libraries like Helmet to protect your Express applications
const helmet = require('helmet');
app.use(helmet());

Don't:

// Ignore security best practices
const express = require('express');
const app = express();

Performance Optimization

Do:

// Use efficient algorithms and data structures
const values = new Set([1, 2, 3]);

Don't:

// Use inefficient methods that can lead to performance issues
const values = [1, 2, 3, 1, 2, 3].filter((value, index, self) => self.indexOf(value) === index);

Typescript Best Practices

Explicit Return Types

Do:

// Declare return types explicitly for functions to improve readability and prevent unintended return values.
function add(a: number, b: number): number {
  return a + b;
}

Don't:

//Omit return types, which can lead to less predictable code and reliance on type inference, which might not always work as expected.
function add(a: number, b: number) {
  return a + b;
}

Interface over Type Alias

Do:

// Prefer interfaces over type aliases when declaring object shapes. Interfaces can be extended and merged, offering more flexibility.
interface User {
  name: string;
  age: number;
}

Don't:

// Use type aliases for object shapes unless you need a feature specific to type aliases, such as union or intersection types.
type User = {
  name: string;
  age: number;
};

Use Utility Types

Do:

// Leverage TypeScript’s utility types like `Partial<T>`, `Readonly<T>`, `Record<K, T>`, etc., to create types based on transformations of other types.
function updateProfile(user: Partial<User>) {
  // ...
}

Don't:

// Manually recreate the functionality provided by utility types, leading to more verbose and less maintainable code.
function updateProfile(user: { name?: string; age?: number }) {
  // ...
}

Non-null Assertion Operator

Do:

// Use non-null assertion operators (!) sparingly, and only when you are certain that a value will not be null or undefined.
function initialize() {
  const el = document.getElementById('myId')!;
  // ...
}

Don't:

function initialize() {
  const el = document.getElementById('myId')!; // Risky if 'myId' does not exist
  // ...
}

Function Parameters as Options Object

Do:

// Use an options object for functions with multiple optional parameters to improve readability and maintainability. Useful when a function has several parameters, especially if many of them are optional.

interface CreateUserOptions {
  name: string;
  email: string;
  password: string;
  age?: number;
  isAdmin?: boolean;
}

function createUser(options: CreateUserOptions): void {
  // Implementation...
}

// Usage
createUser({
  name: 'Alice',
  email: '[email protected]',
  password: 'securePass123',
  age: 42,
});

Don't:

function createUser(name: string, email: string, password: string, age?: number, isAdmin?: boolean): void {
  // Implementation...
}

// Usage - It's unclear what the true parameter stands for.
createUser('Alice', '[email protected]', 'securePass123', 42, true);

DRY and SOLID Principles

The DRY principle emphasizes the need to reduce the repetition of software patterns, replacing them with abstractions or using data normalization to avoid redundancy.

The SOLID principles are a set of guidelines for object-oriented programming that facilitate scalability and maintainability.

DRY Principle

Do:

// Encapsulate repeated logic into functions or classes.
function calculateArea(radius) {
  return Math.PI * radius * radius;
}

const areaOfCircle1 = calculateArea(10);
const areaOfCircle2 = calculateArea(20);

Don't:

// Repeat the same logic in multiple places.
const areaOfCircle1 = Math.PI * 10 * 10;
const areaOfCircle2 = Math.PI * 20 * 20;

Single Responsibility Principle (SRP)

Do:

// Define classes or components with a single responsibility.
class UserAuthenticator {
  authenticateUser(user) {
    // Handle authentication
  }
}

Don't:

// Create classes or components that handle multiple responsibilities.
class UserManager {
  authenticateUser(user) {
    // Handle authentication
  }

  updateUserDetails(user) {
    // Handle updating user details
  }

  deleteUser(user) {
    // Handle deleting a user
  }
}

Open/Closed Principle (OCP)

Do:

// Design modules, classes, functions to be open for extension but closed for modification.
class Shape {
  getArea() {}
}

class Rectangle extends Shape {
  constructor(public width: number, public height: number) {
    super();
  }

  getArea() {
    return this.width * this.height;
  }
}

Don't:

// Modify existing classes when adding new functionality, which risks breaking existing code.
class Shape {
  getArea(shapeType) {
    if (shapeType === 'rectangle') {
      // Calculate area of rectangle
    }
    // Other shapes...
  }
}

Liskov Substitution Principle (LSP)

Do:

// Ensure that subclasses can be used interchangeably with their base class.
class Bird {
  fly() {
    // ...
  }
}

class Duck extends Bird {}
class Ostrich extends Bird {
  fly() {
    throw new Error("Can't fly!");
  }
}

Don't:

// Write subclasses that cannot be used in place of their base class without altering the desired result.
class Bird {
  fly() {
    // ...
  }
}

class Duck extends Bird {}
class Ostrich extends Bird {} // Ostriches can't fly, so this violates LSP.

Interface Segregation Principle (ISP)

Do:

// Create specific interfaces that are tailored to the needs of the client.
interface Printable {
  print(): void;
}

class Book implements Printable {
  print() {
    // Print the book
  }
}

Don't

// Force a class to implement interfaces they don't use.
interface Worker {
  work(): void;
  eat(): void;
}

class Robot implements Worker {
  work() {
    // Perform work
  }

  eat() {
    // Robots don't eat, this method shouldn't exist!
  }
}

Dependency Inversion Principle (DIP)

Do:

// High-level modules should not depend on low-level modules. Both should depend on abstractions. Also, abstractions should not depend on details. Details should depend on abstractions.
interface Database {
  query();
}

class MySqlDatabase implements Database {
  query() {
    // Query logic
  }
}

class UserManager {
  database: Database;

  constructor(database: Database) {
    this.database = database;
  }

  findUser(userId) {
    return this.database.query(`SELECT * FROM users WHERE id = ${userId}`);
  }
}

Don't:

// Hardcode dependencies on low-level modules.
class UserManager {
  findUser(userId) {
    return mySqlDatabase.query(`SELECT * FROM users WHERE id = ${userId}`);
  }
}

const mySqlDatabase = {
  query: (sql) => {
    // Query logic
  }
};

Conclusion

Remember, these are guidelines, not rules. There may be exceptions based on specific scenarios, but strive to follow these practices to write clean, efficient, and maintainable JavaScript code.

You can add more sections as required, following the same "Do/Don't" format to provide clear guidance to your team. Each section should demonstrate a single best practice with succinct and self-explanatory code examples.