JAVASCRIPT / OBJECTS AND PROPERTY MECHANICS
Getters and setters for derived values
Define get and set accessors so derived values like totals or unit conversions are computed on demand from one source of truth instead of stored and synced.
What you will learn
- Write get x() and set x(v) in an object literal or class to make x behave like data
- Keep one source of truth and derive the rest so no copy can go stale
- Invert the derivation inside the setter, and validate the write while you are there
- Spot the traps: self-referential getters, no caching, and getter-only writes
Understanding Getters and setters for derived values
An accessor property has no slot that holds a value. When you write get fahrenheit() inside an object literal, JavaScript stores a function and calls it every time something reads obj.fahrenheit; set fahrenheit(v) is called with the right-hand side every time something assigns to it. The call syntax disappears, which is the whole point: the property looks like plain data to every reader, so you can convert a stored field into a computed one without editing a single line that uses it.
Derived values are where this pays off. Fahrenheit is not an independent fact about a reading, it is celsius restated, and a cart total is just the items summed. If you store both, you own two copies of one fact plus the obligation to update them together on every code path, and the copy that drifts is silently wrong because nothing ever re-derives it. A getter removes the obligation entirely: there is nothing to keep in sync because there is nothing stored.
Since the pair is just two functions, normal function rules apply. this is the object the property was accessed on, so the getter always reads the current backing fields; there is no memoization, so a getter that sorts or sums does that work on every single read; and a getter that reads its own name calls itself until the stack dies. The setter is where the derivation runs backwards, turning 212 into a celsius number or splitting a full name into two fields, which also makes it the natural place to validate, because it is the only door into that field.
const reading = {
celsius: 25,
get fahrenheit() {
return this.celsius * 9 / 5 + 32;
},
set fahrenheit(degrees) {
this.celsius = (degrees - 32) * 5 / 9;
}
};
console.log(reading.fahrenheit);
reading.celsius = 100;
console.log(reading.fahrenheit);
reading.fahrenheit = 32;
console.log(reading.celsius);
console.log(Object.keys(reading).join(', '));
console.log(JSON.stringify(reading));An accessor property stores no value, it runs code on read and write, so a derived value is recomputed from a single source of truth instead of being stored and manually kept in sync.
Worked examples
Setter that splits a derived string
A fullName accessor reads from two fields and writes back into them, rejecting input it cannot invert.
const user = {
first: 'Ada',
last: 'Lovelace',
get fullName() {
return `${this.first} ${this.last}`;
},
set fullName(value) {
const parts = value.trim().split(/\s+/);
if (parts.length !== 2) {
throw new RangeError('fullName needs exactly two words');
}
[this.first, this.last] = parts;
}
};
console.log(user.fullName);
user.fullName = ' Grace Hopper ';
console.log(user.first, '|', user.last);
try {
user.fullName = 'Prince';
} catch (err) {
console.log(err.name + ': ' + err.message);
}
console.log(user.fullName);Example explained
Line 1The getter builds the string fresh from this.first and this.last, so it can never disagree with them.
Line 2Assigning to fullName runs the setter, which normalises the spacing and destructures the two parts into the backing fields.
Line 3A one-word assignment cannot be inverted into first and last, so the setter throws instead of corrupting the object.
Line 4The final read still shows Grace Hopper, proving the rejected write changed nothing.
Getter-only total in strict mode
A read-only derived property tracks a mutating array and refuses assignment loudly under strict mode.
'use strict';
const cart = {
items: [
{ price: 10, qty: 2 },
{ price: 4, qty: 3 }
],
get total() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0);
}
};
console.log(cart.total);
cart.items.push({ price: 5, qty: 1 });
console.log(cart.total);
try {
cart.total = 0;
} catch (err) {
console.log(err.name);
}
console.log(cart.total);Example explained
Line 1The first read sums 10*2 plus 4*3, and no total is stored anywhere.
Line 2Pushing an item changes nothing about the accessor, yet the next read reports 37 because the sum is recomputed.
Line 3cart.total = 0 hits an accessor with no set half, which is a TypeError in strict mode and a silently ignored write outside it.
Line 4The last read still gives 37, so the failed assignment left the items untouched.
Class accessors over private fields
A class getter derives area from private state, and its setter distributes the write across both dimensions.
class Rectangle {
#width;
#height;
constructor(width, height) {
this.#width = width;
this.#height = height;
}
get area() {
return this.#width * this.#height;
}
set area(target) {
const scale = Math.sqrt(target / this.area);
this.#width *= scale;
this.#height *= scale;
}
get size() {
return `${this.#width.toFixed(2)} x ${this.#height.toFixed(2)}`;
}
}
const r = new Rectangle(3, 4);
console.log(r.area);
r.area = 48;
console.log(r.size);
console.log(r.area);
console.log(Object.keys(r).length);Example explained
Line 1The getter multiplies the two private fields, so area cannot be set to a value the shape does not actually have.
Line 2The setter calls its own sibling getter to learn the current area, then scales both dimensions by the square root of the ratio.
Line 3Reading area again returns exactly 48 because the state that produces it really changed.
Line 4Object.keys(r) is empty: class accessors live on the prototype and private fields are not string-keyed properties at all.
Important notes
JSON.stringify and object spread call the getter and store its result as a plain value, so the copy is a snapshot that stops tracking the source.
Getters run on every read with no caching, so if the computation is expensive, either cache deliberately in a backing field or expose a method instead, making the cost visible at the call site.
Common mistakes
Giving the getter the same name as the field it reads, as in get area() { return this.area; }, which re-enters itself and dies with RangeError: Maximum call stack size exceeded.
Keeping the derived value as an ordinary property for speed and updating it by hand in each method, so the one code path that forgets leaves a permanently wrong number that reading can never detect.
Writing only a getter and then assigning to it in non-strict code, where the assignment is silently discarded and the object keeps its old values, so the failure surfaces far away from the line that caused it.
Try it yourself
Change, predict, then run
In a browser console, make a person object with birthYear: 1990 and a get age() that derives the age from new Date().getFullYear(), then add set age(value) that writes birthYear back. Confirm that changing birthYear changes age, and that person.age = 30 updates birthYear instead of storing 30.
Open the JavaScript workspaceCheck your understanding
A cart object stores items and exposes get total(). A teammate replaces the getter with a plain total number that every method updates after it changes items. What is the main risk that introduces?
- Any code path that changes items without updating total leaves total wrong, and reading total can never notice
- Reading total gets slower because the lookup must now walk the prototype chain
- total can no longer appear in the output of JSON.stringify
- Assigning to total will now throw a TypeError in strict mode
Show answer
A getter recomputes from items on every read, so it cannot disagree with them; a stored number is a second copy of the same fact and depends on every future code path remembering to update it. Option 4 is backwards: it is the getter-only accessor that throws on assignment, while a plain data property accepts writes without complaint.