← All material

Core JavaScript · 5 of 9

Pure Functions and Immutability

Classes make shared (and mutable state easy to default to — a method can read this.something instead of taking it as a parameter:

The old way

class Cart {
  constructor(discount: number) {
    this.discount = discount;
  }

  private discount: number;

  addDiscount(items: Item[]) {
    for (const item of items) {
      item.price = item.price * (1 - this.discount);
    }
    return items;
  }
}

const cart = new Cart(0.1);
cart.addDiscount(items);

Remove hidden dependencies

addDiscount(items) doesn't show what discount it applies. You have to look at this.discount, and any method on the class is may have changed it.

Inheritance makes this harder - a subclass can override a method that changes this.discount, invisible at the call site.

class VipCart extends Cart {
  applyLoyaltyBonus() {
    this.discount += 0.05; // some unrelated method changes it
  }
}

const cart = new VipCart(0.1);
cart.applyLoyaltyBonus();
cart.addDiscount(items); // which discount actually applies here?

Finding the answer means reading every method in the hierarchy that might touch this.discount. Testing it means building a Cart into the right state first

Instead call the function with and pass all values it depends on.

Return new data instead of mutating

export const addDiscount = (items, discount) => {
  const result = [];
  for (const item of items) {
    result.push({ ...item, price: item.price * (1 - discount) });
  }
  return result;
};

const discounted = addDiscount(items, 0.1);
// `items` is untouched; `discounted` is a new array

A function that only reads its inputs and returns a new value — a pure function — is safe to call from anywhere, since nothing it touches can surprise another caller. The loop above never touches items; it pushes onto a fresh result array and returns that. A for loop is fine — the rule is build new data and return it, not avoid loops.