const - JS | lectureJavaScript OOP

Setters and Getters

JavaScript OOP

In this lecture, we will talk about a feature that's common to all objects in JavaScript: setters and getters. Every object can have setter and getter properties in JavaScript. And we call them accessor properties, while the other properties we have seen so far are called data properties. So what are setters and getters? As the name suggests, they are functions or methods that get and set a property's value. But on the outside, they look like normal properties. So let's start by seeing how this works in practice with a simple object literal.

Let's consider the following object literal:

index.js
const account = {
  owner: 'Christopher Simpson',
  movements: [200, 450, -400, 3000, -650, -130, 70, 1300],
};

Now let's say we want a method to get the total amount of deposits in the movements array. We could do this by creating a method on the object:

index.js
const account = {
  owner: 'Christopher Simpson',
  movements: [200, 450, -400, 3000, -650, -130, 70, 1300],
  totalDeposits() {    // Do something  },};

To transform this method into a getter, we simply prepend the get keyword to the method name.

index.js
const account = {
  owner: 'Christopher Simpson',
  movements: [200, 450, -400, 3000, -650, -130, 70, 1300],

  get totalDeposits() {    return this.movements      .filter((mov) => mov > 0)      .reduce((acc, mov) => acc + mov, 0);  },};

And now, instead of calling the method like a function, we can access it like a property:

index.js
console.log(account.totalDeposits); // ==> 5020

This can be very useful when we want to read something as a property but still, need to do some calculations. So this is how we can create a getter. Now let's see how we can create a setter. Let's say we want to create a method that sets the latest movement in the movements array.

index.js
const account = {
  owner: 'Christopher Simpson',
  movements: [200, 450, -400, 3000, -650, -130, 70, 1300],

  get totalDeposits() {
    return this.movements
      .filter((mov) => mov > 0)
      .reduce((acc, mov) => acc + mov, 0);
  },
  set latest(mov) {    this.movements.push(mov);  },};

Now, how do we call this method? Well, if it was a regular method, we would call it like this:

index.js
account.latest(40);

But since it's a setter, we can't call it like a regular method. Instead, we need to set it like a property:

index.js
account.latest = 40;

If we now log the movements array, we can see that the latest movement has been added:

index.js
console.log(account.movements);

movements array with latest movement added

So, in a nutshell, this is how setters and getters work for any regular object in JavaScript. However, classes also have setters and getters. And they do indeed work in the same way. So let's see how we can create setters and getters in classes. Let's use our Person class from the previous lectures as an example.

index.js
class Person {
  constructor(firstName, lastName, birthYear, profession) {
    // ...
  }

  calculateAge() {
    console.log(2024 - this.birthYear);
  }
}

const william = new Person('William', 'Park', 1992, 'Designer');

Let's say we want to create a getter that returns a person's full name.

index.js
class Person {
  constructor(firstName, lastName, birthYear, profession) {
    // ...
  }

  calculateAge() {
    console.log(2024 - this.birthYear);
  }

  get fullName() {    return `${this.firstName} ${this.lastName}`;  }}

Now, if we want to access the full name of a person, we can simply do this:

index.js
console.log(william.fullName); // ==> William Park

With this, you can see that a getter is indeed just like any other regular method that we set on the prototype. And in fact, we can see that on the prototype of the william object:

fullName getter on the prototype of the william object

This was a very simple use case for a getter. But setters and getters can actually be very useful when we want to do some data validation. As an example, let's try some validation with a person's profession. Let's say we want to check if a person's profession is a string and if it differs from an empty string. And if it's not, we want to log an error message.

index.js
class Person {
  constructor(firstName, lastName, birthYear, profession) {
    // ...
    this.profession = profession;  }

  calculateAge() {
    console.log(2024 - this.birthYear);
  }

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }

  set profession(value) {    console.log(value);    if (typeof value !== 'string' && value !== '') {      console.log('Invalid profession');    } else {      this.profession = value;    }  }}

What's important to understand here is that we are creating a setter for a property name that already exists. What this means is that each time we set a new value for the profession property in the constructor, the setter (profession) will be called. Hence, the value parameter in the setter will be the new value that we set for the profession property. So let's try this out:

setter called when setting a new value for the profession property

Hmmm, this doesn't seem to work 🤔. We are actually seeing the profession property being logged, but now we are getting a really cryptic error message. Let's try to debug this.

What's actually happening here is that there is a conflict. Both the setter function and the constructor are trying to set the profession property. Hence resulting in the weird error message we just saw. So, what we need to do is to create a new property name, and the convention for this is to add an underscore to the property name. So let's do that:

index.js
class Person {
  constructor(firstName, lastName, birthYear, profession) {
    // ...
    this._profession = profession;
  }

  //...

  set profession(value) {
    console.log(value);
    if (typeof value !== 'string' && value !== '') {
      console.log('Invalid profession');
    } else {
      this._profession = value;    }
  }
}

⚠️ Again, it is just a convention, not a JavaScript feature. It is just a different variable name to avoid the naming conflict. However, doing this creates a new property on the object. So if we now log the william object, we can see that there is a new property called _profession:

new property called _profession on the william object

And now, if we try to log the profession property, we will get undefined:

index.js
console.log(william.profession); // ==> undefined

Hence to fix this, we need to create a getter for the profession property. And in the getter, we can simply return the _profession property:

index.js
class Person {
  constructor(firstName, lastName, birthYear, profession) {
    // ...
    this._profession = profession;
  }

  //...

  set profession(value) {
    console.log(value);
    if (typeof value !== 'string' && value !== '') {
      console.log('Invalid profession');
    } else {
      this._profession = value;
    }
  }

  get profession() {    return this._profession;  }}

console.log(william.profession); // ==> Designer

Let's try to create another user just to test our validation. Let's create a user with an invalid profession:

index.js
const maggie = new Person('Johnston', 'Maggie', 1992, 23);

setter called with invalid profession

As you can see, we get the expected error message. Now, if we log the maggie object, we can see that the _profession property does not exist:

_profession property does not exist on the maggie object

But now, if we add a valid profession, then we indeed get the _profession property:

index.js
const maggie = new Person('Johnston', 'Maggie', 1992, 'Lawyer');
console.log(maggie);

maggie object with _profession property

And just like before we can access the profession property:

index.js
console.log(maggie.profession); // ==> Lawyer

Nice! So this pattern of creating a new property name with an underscore is important to understand whenever we try to set a property that already exists. To finish, we don't need to use getters or setters, and many people actually don't. However, sometimes it's nice to be able to use these features, especially when we want to do some data validation.