Getters and Setters
A property that is secretly a function. Used well, they let you change how something is stored without breaking a single line of code that reads it.
By the end: Expose computed values and validate assignment.
A getter is a method you read without brackets. That sounds like a difference in looks only. It is not, because it means you can change how a value is produced without changing a single line of the code that reads it.
class Bus {
constructor(plate, seats) {
this.plate = plate;
this.seats = seats;
}
// Note: no brackets at the call site.
get size() {
if (this.seats > 30) return 'full coach';
if (this.seats > 14) return 'coaster';
return 'minibus';
}
get label() {
return `${this.plate} (${this.size})`;
}
}
const b = new Bus('LAG-441-XA', 14);
console.log(b.size);
console.log(b.label);
b.seats = 33;
console.log('after a refit:', b.label);
Output
The refactor that getters make free
Start with a stored property. Realise later it should be computed. With a getter, nothing that reads it has to change.
class Cart {
constructor() {
this.items = [];
this.total = 0;
}
add(price) {
this.items.push(price);
this.total += price;
}
}
const cart = new Cart();
cart.add(12);
cart.add(8);
console.log('total:', cart.total);
// Someone edits the array directly, and the total is now a lie.
cart.items.push(100);
console.log('items:', cart.items, '| total says:', cart.total);
Output
class Cart {
#items = [];
add(price) {
this.#items.push(price);
return this;
}
get items() {
return [...this.#items]; // a copy, so callers cannot edit the real one
}
get total() {
return this.#items.reduce((sum, p) => sum + p, 0);
}
get count() {
return this.#items.length;
}
}
const cart = new Cart();
cart.add(12).add(8);
console.log('total:', cart.total, '| count:', cart.count);
// The same attempt now changes nothing, because items hands back a copy.
cart.items.push(100);
console.log('items:', cart.items, '| total:', cart.total);
Output
Setters, and the one job they are good at
A setter runs when someone assigns to the property. Its honest use is to check the value and tidy it up on the way in.
class Bus {
#seats = 14;
constructor(plate) { this.plate = plate; }
get seats() { return this.#seats; }
set seats(value) {
const n = Number(value);
if (!Number.isInteger(n)) throw new TypeError('seats must be a whole number.');
if (n < 1 || n > 59) throw new RangeError(`seats must be 1 to 59, got ${n}.`);
this.#seats = n;
}
}
const b = new Bus('LAG-441-XA');
b.seats = 33;
console.log(b.seats);
b.seats = '44'; // normalised on the way in
console.log(b.seats, typeof b.seats);
try { b.seats = 0; } catch (err) { console.log('refused:', err.message); }
try { b.seats = 2.5; } catch (err) { console.log('refused:', err.message); }
Output
The three ways setters go wrong
class Broken {
set label(value) {
// This assignment calls the setter again. And again.
this.label = value.trim();
}
get label() { return this._nothing; }
}
try {
new Broken().label = ' x ';
} catch (err) {
console.log(err.name + ':', err.message);
}
// The fix is to store somewhere else: a private field, or a differently named one.
class Fixed {
#label = '';
set label(value) { this.#label = String(value).trim(); }
get label() { return this.#label; }
}
const f = new Fixed();
f.label = ' Lagos - Abuja ';
console.log('fixed:', JSON.stringify(f.label));
Output
class ReadOnly {
#value = 42;
get value() { return this.#value; }
}
const r = new ReadOnly();
// In strict mode, which is every module and every class body, this throws.
// In an old sloppy-mode script it was ignored in silence, and the caller
// walked away believing the assignment had worked.
try {
r.value = 99;
} catch (err) {
console.log(err.name + ':', err.message);
}
console.log('value is still', r.value);
// Either way the message is about mechanics, not intent. Say what you mean.
class Loud {
#value = 42;
get value() { return this.#value; }
set value(_) {
throw new TypeError('value is derived and cannot be assigned. Use recalculate().');
}
}
try { new Loud().value = 99; } catch (err) { console.log('refused:', err.message); }
Output
class Slow {
#rows = Array.from({ length: 200 }, (_, i) => i);
get worst() {
// Pretend this is a real calculation.
return this.#rows.reduce((a, b) => a + b, 0);
}
}
const s = new Slow();
let touched = 0;
for (let i = 0; i < 5; i++) {
touched += s.worst; // recomputed every single time
}
console.log('sum of five reads:', touched);
// Cache it when the cost is real and the inputs do not change.
class Cached {
#rows = Array.from({ length: 200 }, (_, i) => i);
#sum = null;
get worst() {
if (this.#sum === null) this.#sum = this.#rows.reduce((a, b) => a + b, 0);
return this.#sum;
}
add(n) { this.#rows.push(n); this.#sum = null; return this; }
}
const c = new Cached();
console.log('cached read:', c.worst, c.worst, c.worst);
c.add(1000);
console.log('after add, recomputed:', c.worst);
Output
Getters on object literals, and defining them by hand
const hold = {
widthM: 2,
depthM: 5,
get areaM2() { return this.widthM * this.depthM; },
set areaM2(_) { throw new TypeError('Set widthM and depthM instead.'); },
};
console.log('luggage floor area:', hold.areaM2);
hold.widthM = 3;
console.log('after widening:', hold.areaM2);
// And the fully explicit form, which is what a getter compiles down to.
const seat = {};
Object.defineProperty(seat, 'label', {
get() { return 'D12'; },
enumerable: true,
});
console.log(seat.label);
console.log('descriptor:', Object.getOwnPropertyDescriptor(seat, 'label'));
Output
class Ticket {
#serial = 'A-1';
constructor(row, seat) { this.row = row; this.seat = seat; }
get label() { return `${this.row}${this.seat}`; }
toJSON() {
return { serial: this.#serial, label: this.label };
}
}
const t = new Ticket('D', 12);
console.log('without toJSON the label would vanish. With it:');
console.log(JSON.stringify(t));
Output
Your turn: temperature, both ways
Write a Reading class that stores celsius in a private field. Give it a celsius getter and setter, and a fahrenheit getter and setter that convert. Setting either should be visible through the other. Reject anything below absolute zero, which is -273.15 C.
Output
Recap
- A getter is read without brackets, which lets you replace a stored value with a computed one without touching any caller.
- Use a getter for facts about the object, a method for actions and for anything slow.
- A getter that hands back internal state should hand back a copy.
- Never assign to the same property name inside its own setter; store in a private field instead.
- Class getters do not appear in
JSON.stringify; addtoJSON()when the serialised shape matters.