Learn by reading through in order

Getters and Setters — Running Code When a Property Is Read or Assigned

Getters and setters run code when a property is read or assigned. Covers computed properties, validating assigned values, going through a setter from the constructor, and read-only properties.

Say a seminar sign-up page shows the number of seats left as "capacity − sign-ups". If you calculate the seats left when you create the instance and store it, the display won't change when more people sign up later. And if someone assigns a capacity smaller than the number of sign-ups, nothing stops it.

This article covers getters and setters, which run code when a property is read or assigned.

Recalculating on Every Read — get and Computed Properties

If you calculate the seats left as "capacity − sign-ups" in the constructor and store it, the seats left stay at the value from creation time even after you change the number of sign-ups. Making it a method lets you recalculate, but then callers have to remember to leave the parentheses off for the capacity and add them for the seats left.

You write a getter (a method in a class with get in front, which runs when the property is read) with no parameters, like get remainingSeats() { ... }, and reading the property gives you whatever it returns.

A property like remainingSeats, which is calculated from other properties and returned, is called a computed property.

class Seminar {
  reserved = 0;                  // Number of sign-ups

  constructor(capacity) {
    this.capacity = capacity;    // Capacity
  }

  // Calculates from the current capacity and sign-ups every time it's read
  get remainingSeats() {
    return this.capacity - this.reserved;
  }
}

const seminar = new Seminar(30);
console.log(seminar.remainingSeats);   // 30 (read without parentheses)

// Read it again after 12 people sign up
seminar.reserved = 12;
console.log(seminar.remainingSeats);   // 18
Store the Seats Left, or Calculate on Every Read?
Store seats leftin constructorStores 30 - 0at creationAssign 12to reservedReading itstill gives 30Define seats leftwith getStores nothingat creationAssign 12to reserved30 - 12 at readtime gives 18
Both rows make the same assignment before reading. A getter calculates with reserved as it is at the moment of reading.

The 30 in the top row was calculated once at new time and stored. The getter in the bottom row stores nothing and runs the return expression every time you read seminar.remainingSeats, so no matter how many times you change reserved, you don't need to add a line that recalculates the seats left.

Calling a Getter with Parentheses Throws a TypeError

You write a getter as a method, but callers don't add parentheses. If you write seminar.remainingSeats(), you're calling the returned 18 as a function, which throws TypeError: seminar.remainingSeats is not a function.

A menu keeps showing the old tax-inclusive prices after a price increase. Fix it without changing the code that reads the prices. MenuItem and menu are already declared.

① Replace priceWithTax, which is calculated in the constructor, with a property that's calculated every time it's read.

② Raise the pre-tax price of every item by 40 yen.

③ Display "Item name: X yen incl. tax" for every item.

④ Display "Tax: X yen" for the first item.

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

Checking Assigned Values — set and Validation

As a seminar gets closer, the venue might let you raise the capacity from 30 to 40. You could write a check with if and throw in the constructor, but it only runs at new time. If you later assign seminar.capacity = 5, which is less than the 12 sign-ups, nothing stops it, and the seats left show as -7.

A setter (a method you write in a class with set in front, which runs when the property is assigned) receives the assigned value as its parameter value. You store the value itself under a different name, such as _capacity, and return it from a getter with the property's name, get capacity.

A leading _ is a convention that means "don't touch this directly from outside".

class Seminar {
  constructor(capacity, reserved) {
    this.reserved = reserved;     // Number of sign-ups
    this._capacity = capacity;    // The capacity value itself goes in _capacity
  }

  get capacity() { return this._capacity; }

  set capacity(value) {           // The assigned value arrives in value
    if (value < this.reserved) {
      throw new Error(`Capacity must be at least the ${this.reserved} sign-ups: ${value}`);
    }
    this._capacity = value;
  }
}

const seminar = new Seminar(30, 12);
seminar.capacity = 40;             // set capacity runs and stores 40
console.log(seminar.capacity);     // 40 (get capacity runs)
seminar.capacity = 5;              // Error: Capacity must be at least the 12 sign-ups: 5
Which Method Runs on Assignment and on Read
get capacityset capacity(value)① seminar.capacity= 40② Readseminar.capacity③ seminar.capacity= 5set runs:40 is 12 or moreget runs andreturns _capacityset runs:5 is less than 12_capacitybecomes 40Returns 40Throws;stays at 40
When the property is on the left of =, set runs; when it's read anywhere else, get runs. A value that fails the check never reaches _capacity.

Line ③ throws, so unless you wrap it in try, the lines after it don't run. As long as you assign to capacity, the value goes through the setter, but if you write seminar._capacity = 5 directly, it skips the check. The next article covers how to prevent that.

Assigning to the Same Name Inside a Setter Throws a RangeError

If you write this.capacity = value; inside set capacity(value), that assignment calls the setter again, which assigns again, and so on until it throws RangeError: Maximum call stack size exceeded. Store the value under a different name, such as _capacity.

You apply input from an air conditioner's control panel to the set temperature. AirConditioner and temperatureInputs are already declared.

① Add a temperature property that reads and writes the set temperature and throws the exception "Temperature must be 16 to 30 degrees: X" if the value is out of range.

② Assign the inputs in order, and display "Set to X degrees" if it's applied, or the exception message if it isn't.

③ Display "Room name: running at X degrees" with the final set temperature.

JavaScript / TypeScript Editor

Run code to see output

Going Through the Setter at Creation Too — Assigning in the constructor

The Seminar above has its constructor store the value directly in _capacity, so new Seminar(5, 12) creates an instance without any check. If you also write the same if in the constructor, you have to fix two places every time the condition changes, and if you forget one, new and assignment give different results.

If you assign to the name without _ inside the constructor, as in this.capacity = capacity, the setter runs for that line too. The check lives in one place, the setter, and values passed to new and values assigned later are checked against the same condition.

class Seminar {
  constructor(capacity, reserved) {
    this.reserved = reserved;     // Store the sign-ups used by the check first
    this.capacity = capacity;     // The name has no _, so set capacity runs
  }

  get capacity() { return this._capacity; }
  set capacity(value) {
    if (value < this.reserved) {
      throw new Error(`Capacity must be at least the ${this.reserved} sign-ups: ${value}`);
    }
    this._capacity = value;
  }
}

try {
  new Seminar(5, 12);             // new also goes through the setter's check
} catch (error) {
  console.log(error.message);     // Capacity must be at least the 12 sign-ups: 5
}
An Assignment in the constructor Calls the Setter
Body of try (new is called here)
  • new Seminar(5, 12) — calls the constructor
  • The setter's exception comes back up to here, and no instance is returned
Body of the called constructor
  • this.reserved = 12 — stores the sign-ups before the check
  • this.capacity = 5 — the name has no _, so it calls set capacity
Body of the called set capacity(value)
  • value is 5 and this.reserved is 12
  • 5 < 12, so throw runs — nothing goes into _capacity
The this.capacity = 5 line calls set capacity. The exception leaves the constructor and reaches try.

If you move this.reserved = reserved below this.capacity = capacity, this.reserved is undefined when the setter runs. In a numeric comparison, undefined becomes NaN, and 5 < NaN is false, so the instance is created with a capacity of 5.

You register employees' full names split into first and last names. staffNames is already declared.

① Define Employee, which takes a full name and stores the first and last names separately.

② Add fullName, which reads and writes "First Last" and throws the exception "Separate the first and last names with a space: X" if there's no space.

③ Create the employees one by one, and display the exception message for any that can't be created.

④ Assign "Alice Johnson" to the first employee's fullName, then display everyone's first, last, and full names.

JavaScript / TypeScript Editor

Run code to see output

Blocking Assignment — Properties with Only a Getter

The seats left are determined by the capacity and the sign-ups. If the page's code can write seminar.remainingSeats = 100, the page shows 100 seats left for a capacity of 30 with 12 sign-ups, and the three values no longer add up.

A property with only a getter and no setter of the same name is read-only (it can be read but not assigned). In class bodies and in strict mode (a mode that turns some mistakes into exceptions), which this runtime uses, the line that assigns to it throws a TypeError. Otherwise, the assignment is silently ignored.

class Seminar {
  constructor(capacity, reserved) {
    this.capacity = capacity;
    this.reserved = reserved;
  }

  get remainingSeats() { return this.capacity - this.reserved; }   // No setter
}

const seminar = new Seminar(30, 12);
try {
  seminar.remainingSeats = 100;          // Assigning to a name with no set
} catch (error) {
  console.log(error.name);               // TypeError
}
console.log(seminar.remainingSeats);     // 18 (same as before the assignment)

// To change the seats left, change the sign-ups they're based on
seminar.reserved = 20;
console.log(seminar.remainingSeats);     // 10
Is an Assignment to the Seats Left Accepted or Rejected?
Store seats leftas a valueremainingSeats= 100100 goes inas-isDoesn't match30 seats, 12 takenDefine seats leftwith only getremainingSeats= 100No set, soTypeErrorStays at30 - 12 = 18
In the top row, the assignment goes through, leaving a value that doesn't match the capacity or sign-ups. With only a getter, the assignment is rejected on the spot.

The bottom row throws because a name with only a getter has nowhere to put an assigned value, and no new key is created on the instance either. The table below lists the approaches used in this article's Seminar, along with a method for comparison.

ApproachHow Callers Use ItGood For
Plain property (reserved)seminar.reserved = 20 stores it as-isValues that don't need checks
Getter only (remainingSeats)Read-only: seminar.remainingSeatsValues computed from others
Getter and setter (capacity)seminar.capacity = 40 is checkedValues to check on every assignment
Method (e.g., reserve(count))Called with parentheses and argumentsResults that depend on arguments
QUIZ

Knowledge Check

Answer each question one by one.

Q1With a getter that returns 30 - this.reserved, what do you get if you set reserved to 20 and then read the getter?

Q2If set capacity contains this.capacity = value; and you assign 40, what happens?

Q3What happens if you assign 100 to seminar.remainingSeats, which has only a getter?