10 Ways to Write Clean JavaScript Code

@avindrafernando

Who Am I?

@avindrafernando

@avindra1

avindrafernando

Maintaining code can be challenging

Clean Code

Clean code is code that is easy to understand and easy to change.

- Clean Code

What is Clean Code?

  • Easy to understand
    • The execution flow of the entire application
    • How the different objects collaborate with each other
    • The role and responsibility of each class/component
    • What each method/function does
    • What is the purpose of each expression and variable

10
Ways

Variables

Use meaningful and pronounceable variable names

// NOT SO CLEAN
const yyyymmdd = moment().format("YYYY/MM/DD");

const m = new Date().getMonth();

let ok;

if (test.coverage > 75) {
  ok = true;
}
// MUCH BETTER

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

const currentMonth = new Date().getMonth();

const TEST_COVERAGE_THRESHOLD = 75;

const isTestCoverageThresholdMet = test.coverage > TEST_COVERAGE_THRESHOLD;

Use searchable names

// NOT SO CLEAN
// What does this magic number mean?
setTimeout(doSomething, 86400);
// MUCH BETTER
// Declare them as capitalized named constants.
const SECONDS_PER_DAY = 60 * 60 * 24; //86400;

setTimeout(doSomething, SECONDS_PER_DAY);

Don't add unneeded context

// NOT SO CLEAN
const restaurant = {
  restaurantName: "Best restaurant in town",
  restaurantCapacity: 100,
  restaurantRating: 5
};

function rateRestaurant(restaurant, rating) {
  restaurant.restaurantRating = rating;
}
// MUCH BETTER
const restaurant = {
  name: "Best restaurant in town",
  capacity: 100,
  rating: 5
};

function rateRestaurant(restaurant, rating) {
  restaurant.rating = rating;
}

Functions

Use two or fewer arguments

// NOT SO CLEAN
function createNavigationTile(title, subTitle, body, isActive) {
  // ...
}

createNavigationTile("Foo", "Bar", "Baz", true);
// MUCH BETTER
function createNavigationTile({ title, subTitle, body, isActive }) {
  // ...
}

createNavigationTile({
  title: "Foo",
  subTitle: "Bar",
  body: "Baz",
  isActive: true
});

Use default arguments

// NOT SO CLEAN
function createBuilding(name) {
  const buildingName = name ?? "Awesome Building Name"
  // ...
}
// MUCH BETTER
function createBuilding(name = "Awesome Building Name") {
  const buildingName = name;
  // ...
}

Use Object.assign for default objects(1)

// NOT SO CLEAN
const storeConfig = {
  name: null,
  location: "Newark",
  description: "Store Description",
  isActive: false
};

function createStore(config) {
  config.name = config.name || "My Store";
  config.location = config.location || "Unknown";
  config.description = config.description || "My store is the best.";
  config.isActive =
    config.isActive !== undefined ? config.isActive : true;
}

createStore(storeConfig);

Use Object.assign for default objects(2)

// MUCH BETTER
const storeConfig = {
  name: "Hyvee",
  //No location provided
  description: "Best store.",
  isActive: true
};

function createStore(config) {
  let finalStoreConfig = Object.assign(
    {
      name: "My Store",
      location: "Newark",
      description: "My store is the best.",
      isActive: true
    },
    config
  );
  return finalStoreConfig;
  // {name: "Hyvee", location: "Newark", description: "Best store.", isActive: true}
}

createStore(storeConfig);

Use explanatory variables

// NOT SO CLEAN
const address = "Terminal Way, Kansas City 64112";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
validateCityZipCode(
  address.match(cityZipCodeRegex)[1],
  address.match(cityZipCodeRegex)[2]
);
// MUCH BETTER
const address = "Terminal Way, Kansas City 64112";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) ?? [];
validateCityZipCode(city, zipCode);

Functions should do one thing

// NOT SO CLEAN
function notifyGroceryStores(groceryStores) {
  groceryStores.forEach(groceryStore => {
    const groceryStoreRecord = database.lookup(groceryStore);
    if (groceryStoreRecord.isActive()) {
      notify(groceryStore);
    }
  });
}
// MUCH BETTER
function notifyActiveGroceryStores(groceryStores) {
  groceryStores.filter(isActiveGroceryStore).forEach(notify);
}

function isActiveGroceryStore(groceryStore) {
  const groceryStoreRecord = database.lookup(groceryStore);
  return groceryStoreRecord.isActive();
}

Avoid using flags as function parameters

// NOT SO CLEAN
function createFile(name, temp) {
  if (temp) {
    fs.create(`./temp/${name}`);
  } else {
    fs.create(name);
  }
}
// MUCH BETTER
function createFile(name) {
  fs.create(name);
}

function createTempFile(name) {
  createFile(`./temp/${name}`);
}

Avoid side effects (1)

// NOT SO CLEAN
let animalName = "cat";

function replaceFirstLetterInAnimalName() {
  animalName = animalName.replace(/^./, 'r');;
}

replaceFirstLetterInAnimalName();

console.log(animalName); // 'rat';
// MUCH BETTER
function replaceFirstLetterInAnimalName(name) {
  return name.replace(/^./, 'r');
}

const animalName = "cat";
const modifiedAnimalName = replaceFirstLetterInAnimalName(animalName);

console.log(animalName); // 'cat'
console.log(modifiedAnimalName); // 'rat';

Avoid side effects (2)

// NOT SO CLEAN
const addItemToList = (list, item) => {
  list.push({ item, date: Date.now() });
};
// MUCH BETTER
const addItemToList = (list, item) => {
  return [...list, { item, date: Date.now() }];
};

Encapsulate conditionals

// NOT SO CLEAN
if (state === "loading" && isEmpty(data)) {
  // ...
}
// MUCH BETTER
function shouldShowLoadingSpinner(state, data) {
  return state === "loading" && isEmpty(data);
}

if (shouldShowLoadingSpinner(state, data)) {
  // ...
}

Conditionals

Avoid negative conditionals

// NOT SO CLEAN
function isAddressNotVerified(address) {
  // ...
}

if (!isAddressNotVerified(address)) {
  // ...
}
// MUCH BETTER
function isAddressVerified(address) {
  // ...
}

if (isAddressVerified(address)) {
  // ...
}

Use optional chaining

// NOT SO CLEAN
const email = (user && user.email) ?? "";
const street =
  (user &&
    user.address &&
    user.address.street) ??
  "";
const state =
  (user &&
    user.address &&
    user.address.state) ??
  "";
// MUCH BETTER
const email = user?.email ?? "";
const street = user?.address?.street ?? "";
const state = user?.address?.state ?? "";

Use shorthand whenever possible

// NOT SO CLEAN
if (isActive === true) {
  // ...
}

if (name !== "" && name !== null && name !== undefined) {
  // ...
}

const isUserEligible = user.isVerified() && user.didSubscribe() ? true : false;
// MUCH BETTER
if (isActive) {
  // ...
}

if (!!name) {
  // ...
}

const isUserEligible = user.isVerified() && user.didSubscribe();

Return early (1)

// NOT SO CLEAN
 function Todos() {
   const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
 
   return (
     <>
       {!isLoading ? (
         <ul>
           {data.map(todo => (
             <li key={todo.id}>{todo.title}</li>
           ))}
         </ul>
       ) : (
	     <span>Loading...</span>
       )}
     </>
   )
 }

Return early (2)

// MUCH BETTER
 function Todos() {
   const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)
 
   if (isLoading) {
     return <span>Loading...</span>
   }
 
   if (isError) {
     return <span>Error: {error.message}</span>
   }
 
   return (
     <ul>
       {data.map(todo => (
         <li key={todo.id}>{todo.title}</li>
       ))}
     </ul>
   )
 }

Classes

Prefer ES2015/ES6 classes (1)

// NOT SO CLEAN
const Vehicle = function(make) {
  if (!(this instanceof Vehicle)) {
    throw new Error("Instantiate Vehicle with `new`");
  }

  this.make = make;
};

Vehicle.prototype.move = function move() {};

const Car = function(make, color) {
  if (!(this instanceof Car)) {
    throw new Error("Instantiate Car with `new`");
  }

  Vehicle.call(this, make);
  this.color = color;
};

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Car.prototype.getColor = function getColor() {};

Prefer ES2015/ES6 classes (2)

// MUCH BETTER
class Vehicle {
  
  constructor(make) {
    this.make = make;
  }
  
  move() {
    /* ... */
  }
}

class Car extends Vehicle {
  
  constructor(make, color) {
    super(make);
    this.color = color;
  }
  
  getColor() {
    /* ... */
  }
}

Concurrency

Use Promises, not callbacks (1)

// NOT SO CLEAN
getStore(function (err, store) {
  getTransactions(store, function (err, transactions) {
    getReports(transactions, function (err, reports) {
      sendReports(reports, function (err) {
        console.error(err);
      });
    });
  });
});

Use Promises, not callbacks (2)

// MUCH BETTER
getStore()
  .then(getTransactions)
  .then(getReports)
  .then(sendReports)
  .catch((err) => console.error(err));

Async/Awaits are even cleaner

// EVEN BETTER
async function sendReportsToStoreManagement() {
  try {
    const store = await getStore();
    const transactions = await getTransactions(store);
    const reports = await getReports(transactions);
    return sendReports(reports);
  } catch (err) {
    console.error(err);
  }
}

Error Handling

Don't ignore caught errors

// NOT SO CLEAN
try {
  functionThatMightThrowError();
} catch (error) {
  console.log(error);
}
// MUCH BETTER
try {
  functionThatMightThrowError();
} catch (error) {
  // Better than console.log:
  console.error(error);
  // Another option:
  notifyUserOfError(error);
  // Another option:
  reportErrorToService(error);
  // OR do all three!
}

Don't ignore rejected promises

// NOT SO CLEAN
getdata()
  .then(data => {
    functionThatMightThrowError(data);
  })
  .catch(error => {
    console.log(error);
  });
// MUCH BETTER
getdata()
  .then(data => {
    functionThatMightThrowError(data);
  })
  .catch(error => {
    // Better than console.log:
    console.error(error);
    // Another option:
    notifyUserOfError(error);
    // Another option:
    reportErrorToService(error);
    // OR do all three!
  });

Formatting

Use consistent case

// NOT SO CLEAN
const MIN_TEMPERATURE = 32;
const maxTemperature = 120;

const fruits = ["Apple", "Banana", "Kiwi"];
const Vegetables = ["Potato", "Cabbage", "Onion"];

function resetData() {}
function refetch_data() {}

class vehicle {}
class Car {}
// MUCH BETTER
const MIN_TEMPERATURE = 32;
const MAX_TEMPARATURE = 120;

const fruits = ["Apple", "Banana", "Kiwi"];
const vegetables = ["Potato", "Cabbage", "Onion"];

function resetData() {}
function refetchData() {}

class Vehicle {}
class Car {}

Use a linter

Comments

Only comment business logic (1)

// NOT SO CLEAN
function generateHash(data) {
  // The hash
  let hash = 0;

  // Length of string
  const length = data.length;

  // Loop through every character in data
  for (let i = 0; i < length; i++) {
    // Get character code.
    const char = data.charCodeAt(i);
    // Make the hash
    hash = (hash << 5) - hash + char;
    // Convert to 32-bit integer
    hash &= hash;
  }
}

Only comment business logic (2)

// MUCH BETTER
function generateHash(data) {
  let hash = 0;
  const length = data.length;

  for (let i = 0; i < length; i++) {
    const char = data.charCodeAt(i);
    hash = (hash << 5) - hash + char;

    // Convert to 32-bit integer
    hash &= hash;
  }
}

Don't leave commented out code

// NOT SO CLEAN
/**
 * 2022-01-01: Initial Commit (AF)
 * 2022-01-02: Added new feature (JD)
 * 2022-01-03: Fixed bug (AF)
 * 2022-01-05: Added more functionality (JD)
 */
function computeAverage(numbers = []) {
  // let sum = 0;
  // for (let i = 0; i < numbers.length; i++) {
  //   sum += numbers[i];
  // }
  const sum = addNumbers(numbers);
  return sum / numbers.length;
}
// MUCH BETTER
function computeAverage(numbers = []) {
  return addNumbers(numbers) / numbers.length;
}

Comment when necessary

// MUCH BETTER
/**  
 * Returns if input is a prime number or not.  
 *  
 * @param {number} number - The number to be checked. 
 * @return {bool} whether the number is a prime number.  
 */ 
function isPrime(number) {   
    // ...
}

Testing

Single concept per test (1)

// NOT SO CLEAN
describe("user info form", () => {
  it('validates form workflows', () => {
    const {handleSubmit, user} = setupSuccessCase()
    expect(handleSubmit).toHaveBeenCalledTimes(1)
    expect(handleSubmit).toHaveBeenCalledWith(user)

    const {handleSubmit, errorMessage} = setupWithNoUsername()
    expect(errorMessage).toHaveTextContent(/username is required/i)
    expect(handleSubmit).not.toHaveBeenCalled()

    const {handleSubmit, errorMessage} = setupWithNoPassword()
    expect(errorMessage).toHaveTextContent(/password is required/i)
    expect(handleSubmit).not.toHaveBeenCalled()
  })
});

Single concept per test (2)

// MUCH BETTER
describe("user info form", () => {
  it('calls onSubmit with the username and password', () => {
    const {handleSubmit, user} = setupSuccessCase()
    expect(handleSubmit).toHaveBeenCalledTimes(1)
    expect(handleSubmit).toHaveBeenCalledWith(user)
  })

  it('shows an error message when submit is clicked and no username is provided', () => {
    const {handleSubmit, errorMessage} = setupWithNoUsername()
    expect(errorMessage).toHaveTextContent(/username is required/i)
    expect(handleSubmit).not.toHaveBeenCalled()
  })

  it('shows an error message when password is not provided', () => {
    const {handleSubmit, errorMessage} = setupWithNoPassword()
    expect(errorMessage).toHaveTextContent(/password is required/i)
    expect(handleSubmit).not.toHaveBeenCalled()
  })
});

F.I.R.S.T principle

  • Fast
  • Isolated
  • Repeatable
  • Self-validating
  • Timely/ Thorough

Type checking

Avoid type checking

// NOT SO CLEAN
function combine(firstInput, secondInput) {
  if (
    (typeof firstInput === "number" && typeof secondInput === "number") ||
    (typeof firstInput === "string" && typeof secondInput === "string")
  ) {
    return firstInput + secondInput;
  }

  throw new Error("Must be of type String or Number");
}

Use TypeScript instead

There are only two hard things in Computer Science: cache invalidation and naming things.
- Phil Karlton

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. ”

— Martin Fowler

 I’m not a great programmer; I’m just a good programmer with great habits.

— Martin Fowler