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.
- Javascript Best Practices
- General Best Practices
- Typescript Best Practices
- DRY and SOLID Principles
- Conclusion
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);
});
}
Do:
const person = { name: 'Alice', age: 25 };
console.log(person.name);
Don't:
const person = { name: 'Alice', age: 25 };
console.log(person['name']);
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]);
}
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);
});
}
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.
Do:
const greeting = `Hello, ${name}!`;
Don't:
const greeting = 'Hello, ' + name + '!';
Do:
const { name, age } = person;
Don't:
const name = person.name;
const age = person.age;
Do:
const newArray = [...oldArray, newItem];
const { x, y, ...rest } = obj;
Don't:
const newArray = oldArray.concat(newItem);
const x = obj.x, y = obj.y;
Do:
const add = (a, b) => a + b;
Don't:
function add(a, b) {
return a + b;
}
Do:
// es6 module syntax
import myFunction from './myModule';
Don't:
// CommonJS syntax
const myFunction = require('./myModule');
Do:
function greet(name = 'Guest') {
console.log(`Hello, ${name}!`);
}
Don't:
function greet(name) {
name = name || 'Guest';
console.log(`Hello, ${name}!`);
}
Do:
try {
// code that may throw an error
} catch (error) {
// error handling
}
Don't:
// code that may throw an error
// No error handling
Do:
function add(a, b) {
return a + b;
}
Don't:
let result;
function add(a, b) {
result = a + b;
}
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);
});
});
}
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
}
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.
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.
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';
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
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;
}
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;
}
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
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();
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);
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;
}
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;
};
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 }) {
// ...
}
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
// ...
}
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);
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.
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;
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
}
}
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...
}
}
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.
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
}
}
// 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!
}
}
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
}
};
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.