29. Class Concepts
Probably want to review getters, setters, static methods, method chaining and this
after this chapter to really get a feel for what these concepts are.
Setter
MDN Docs This is used when you want to validate the input. Let's say you have an age property, and somehow someone tries to enter "twenty-seven" instead of 27 - a string instead of a number.
class User {
set age(value) {
console.log("age setter");
this._age = Number.parseInt(value, 10);
}
}
Notice that there is a _
in the _age
. This means that the property is internal from the class and should not be referenced from the outside. This is super important, because if you accidentally set this result on this.age
, you end up with an infinite loop.
// to reference
const user = new User();
user.age = "18";
console.log(user); // {_age: 18}
Notice how we are able to set the age property on the user instance by setting user.age
= to a certain value.
In this example, we set it to a string "18", which then gets converted to a number by the setter. So, setting user.age
= will call the set age(value)
function and the value will receive the string "18".
Getter
class User {
get age() {
console.log("age getter");
return this._age;
}
set age(value) {
console.log("age setter");
this._age = Number.parseInt(value, 10);
}
}
// using
const user = new User();
user.age = "20"; // calls set age(value)
console.log(user.age); // calls get age(), returns 20 (number)
Another usage for a getter is to change the formatting of a property. The setter receives a value, the getter does not. This is because the value (or any parameter name you choose) provided to the setter is the value written after the equal sign. However, for the getter, there is no equal sign. You're only reading the value.
Putting Getters and Setters Together
You can still use getters and setters with the pattern where you capture constructor parameters. That's because, whenever you access this.age from within the class, it will also automatically trigger the set age(value) function. This is why we had to create an internal property and call it this._age to avoid creating an infinite loop.
class User {
constructor(age) {
this.age = age; // calls set age(value)
}
get age() {
console.log("age getter");
return this._age;
}
set age(value) {
console.log("age setter");
this._age = Number.parseInt(value, 10);
}
}
Recap
- Defining getters and setters in a class is mostly used to validate or modify certain values before they are set as properties on a class.
- Assuming an instance user, accessing user.age will call get age() if the getter exists.
- Assuming an instance user, setting user.age = will call set age(value) if the setter exists.
- When creating getters and setters, make sure to prefix the new instance variable with an _ to prevent creating an infinite loop.
Try it - tasks and todos
Implement a getter for the todos property in the Tasks class. This getter should return a CSV string of the todos. So each todo should be separated by a comma and space character.
class Tasks {
/**
* @param {string[]} todos
*/
constructor(todos) {
this._todos = todos;
}
get todos() {
return this._todos.join(", ")
};
}
// Sample usage - do not modify
const tasks = new Tasks(["Laundry", "Clean kitchen"]);
console.log(tasks.todos); // "Laundry, Clean kitchen"
Cents
class Payment {
/**
* @param {number} amount
*/
constructor(amount) {
this.amount = amount;
}
get amount() {
return this._cents;
}
set amount(cents) {
this._cents = cents * 100;
}
/* TODO: create getter and setter for 'amount' */
}
// Sample usage - do not modify
const payment = new Payment(10); // 10 USD
console.log(payment.amount); // 1000 (value in cents)
// apply discount
payment.amount = 7; // 7 USD
console.log(payment.amount); // 700 (value in cents)
Static methods and chaining
You can call methods directly on the class, not on the instance. Static methods are prefixed by the keyword static
.
class Config {
static getYear() {
// code to get the current year (for example, 2025)
const date = new Date();
return date.getFullYear();
}
}
When should we use static methods?
- Is the result of this method the same across all instances of the class? If yes, then it should be static.
- Is the method not accessing any instance variable of this class? If yes, then most likely it should be static.
- If you find yourself trying to use
this
within a static method, you probably shouldn't be using a static method. Note: the tldr is that you can't usethis
inside static methods :)
Method Chaining
This is method chaining:
const course = new Course("Learn JavaScript", false);
course.markAsCompleted().setGrade(18).requestCertificate();
// is essentially the same thing as calling
course.markAsCompleted();
course.setGrade(18);
course.requestCertificate();
For this to work, the instance methods should always return this
Here is the class Course:
class Course {
constructor(name, isCompleted) {
this.name = name;
this.isCompleted = isCompleted;
}
markAsCompleted() {
this.isCompleted = true;
return this; // allows method chaining
}
setGrade(grade) {
this.grade = grade;
return this; // allows method chaining
}
requestCertificate() {
this.askedForCertificate = true;
return this; // allows method chaining
}
}
Recap
- Static methods are called directly on the class and cannot be called on an instance.
- Is the result of a method the same across all instances of the class? If yes, then it should be static.
- Is the method not accessing any instance variable of this class? If yes, then most likely it should be static.
- Method chaining is possible when the instance method returns this.
Change to Static
// problem
class Discount {
constructor() {
this.amount = 1_000;
}
applyDiscount() {
if (this.isValid()) {
this.amount = 500;
}
}
isValid() {
return Math.random() <= 0.5; // 50% chance returns true
}
}
// Sample usage - do not modify
console.log(Discount.isValid()); // true or false
const discount = new Discount;
discount.applyDiscount();
console.log(discount.amount); // either 1000 or 500
//solution
class Discount {
constructor() {
this.amount = 1_000;
}
applyDiscount() {
if (Discount.isValid()) {
this.amount = 500;
}
}
static isValid() {
return Math.random() <= 0.5; // 50% chance returns true
}
}
// Sample usage - do not modify
console.log(Discount.isValid()); // true or false
const discount = new Discount;
discount.applyDiscount();
console.log(discount.amount); // either 1000 or 500
Method Chaining Must Return This
class BookSale {
constructor() {
this.amount = 1_000; // US cents
this.currency = "usd";
this.isStudent = false;
}
applyStudentDiscount() {
this.isStudent = true;
this.amount = 800;
return this;
}
setCurrency(currency) {
this.currency = currency;
return this;
}
applyPercentageDiscount(percent) {
this.amount = this.amount - this.amount * percent / 100;
return this;
}
}
// Sample usage - do not modify
const bookSale = new BookSale;
bookSale.applyStudentDiscount().setCurrency("eur").applyPercentageDiscount(5);
Chapter Recap
- Defining getters and setters in a class is mostly used to validate or modify certain values before they are set as properties on a class.
- Assuming an instance user, accessing user.age will call get age() if the getter exists.
- Assuming an instance user, setting user.age = will call set age(value) if the setter exists.
- When creating getters and setters, make sure to prefix the new instance variable with an _ to prevent creating an infinite loop.
- Static methods are called directly on the class and cannot be called on an instance.
- Is the result of a method the same across all instances of the class? If yes, then it should be static.
- Is the method not accessing any instance variable of this class? If yes, then most likely it should be static.
- Method chaining is possible when the instance method returns this.