Why Use Getters and Setters in JavaScript and TypeScript?

Blog

Posted by Nuno Marques on 13 Feb 2023

Why Use Getters and Setters in JavaScript and TypeScript?

Getters and setters are often overlooked in JavaScript and TypeScript, but they can be powerful tools when used correctly. They help encapsulate data, control access to properties, and add logic when getting or setting a value. But do you always need them? Let’s explore their use cases, benefits, performance considerations, alternatives, and when to avoid them.

Estimated reading time: 8 min


What Are Getters and Setters?

In JavaScript and TypeScript, getters and setters allow you to define computed properties that are accessed like regular properties but execute logic under the hood.

Basic Example

class Person {
  private _name: string;
  
  constructor(name: string) {
    this._name = name;
  }

  get name(): string {
    return this._name;
  }

  set name(newName: string) {
    if (!newName) {
      throw new Error("Name cannot be empty");
    }
    this._name = newName;
  }
}

const person = new Person("Alice");
console.log(person.name); // Alice
person.name = "Bob";
console.log(person.name); // Bob

Here, name looks like a regular property but is actually a method executing logic when accessed or modified.


Why Use Getters and Setters?

1. Encapsulation & Data Protection

Getters and setters allow you to hide the internal implementation details of a class while providing controlled access.

Example:

class BankAccount {
  private _balance: number = 1000;
  
  get balance(): number {
    return this._balance; // Only allows reading, prevents direct modification
  }
}

const account = new BankAccount();
console.log(account.balance); // 1000
account.balance = 5000; // ❌ Error (if no setter is defined)

Here, direct modification is blocked, ensuring data integrity.

2. Computed Properties

You can derive values dynamically rather than storing them in separate properties.

Example:

class Rectangle {
  constructor(private width: number, private height: number) {}
  
  get area(): number {
    return this.width * this.height;
  }
}

const rect = new Rectangle(10, 20);
console.log(rect.area); // 200

Instead of storing the area, we compute it dynamically.

3. Input Validation

You can add checks before allowing a property to change.

Example:

class Temperature {
  private _celsius: number;

  constructor(celsius: number) {
    this._celsius = celsius;
  }

  get fahrenheit(): number {
    return (this._celsius * 9) / 5 + 32;
  }

  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("Temperature below absolute zero is not possible");
    }
    this._celsius = value;
  }
}

const temp = new Temperature(25);
console.log(temp.fahrenheit); // 77

temp.celsius = -300; // ❌ Error

This ensures invalid temperatures cannot be assigned.

4. Lazy Initialization

Instead of computing a value immediately, you can compute it when it’s accessed for the first time.

Example:

class User {
  private _cachedData?: string;

  get data(): string {
    if (!this._cachedData) {
      console.log("Fetching data...");
      this._cachedData = "User Data";
    }
    return this._cachedData;
  }
}

const user = new User();
console.log(user.data); // Fetching data... User Data
console.log(user.data); // User Data (cached)

This avoids unnecessary computations until needed.


Performance Considerations

Using getters and setters can have a minor impact on performance because they introduce method calls rather than direct property access. While in most cases this is negligible, in performance-critical scenarios, such as real-time applications, physics simulations, or high-frequency game loops, direct property access may be preferable.

Benchmarking Getter/Setter vs. Direct Access

class Test {
  private _value: number = 0;
  
  get value(): number {
    return this._value;
  }

  set value(v: number) {
    this._value = v;
  }
}

const test = new Test();
const iterations = 1_000_000;

console.time("Direct Access");
for (let i = 0; i < iterations; i++) {
  test["_value"] = i;
}
console.timeEnd("Direct Access");

console.time("Getter/Setter");
for (let i = 0; i < iterations; i++) {
  test.value = i;
}
console.timeEnd("Getter/Setter");

This benchmark helps determine whether getters/setters impact performance significantly in your case.


Alternatives to Getters and Setters

If you need encapsulation but don't want the method call overhead, here are alternatives:

1. Public Properties (Direct Access)

If no logic is required, just use public properties.

class Car {
  constructor(public make: string) {}
}
const car = new Car("Toyota");
console.log(car.make);
car.make = "Honda";

2. Using Methods Instead of Getters/Setters

Instead of get and set, you can use regular methods.

class Product {
  private price: number;
  
  constructor(price: number) {
    this.price = price;
  }

  getPrice(): number {
    return this.price;
  }
  
  setPrice(newPrice: number) {
    if (newPrice < 0) {
      throw new Error("Price cannot be negative");
    }
    this.price = newPrice;
  }
}

Using methods makes it clear that you're calling a function, not accessing a property.


Key Takeaways

βœ… Use Getters and Setters When:

  • You need encapsulation to prevent direct modifications.
  • You want computed properties instead of storing redundant values.
  • Validation or formatting is required before setting a value.
  • You need lazy initialization for efficiency.

❌ Avoid Them When:

  • A simple public property is enough.
  • Performance is critical, and method calls add overhead.
  • They overcomplicate the code without adding value.

Final Thoughts

Getters and setters are a great tool, but they aren’t always necessary. Use them when they improve clarity and safety, but avoid them if they make your code unnecessarily complex or slow.