Property Getters and Setters in Practice

Posted by

How to use get and set for clean APIs, validation, caching, and more—with real-world patterns in JavaScript and TypeScript.

How to use get and set for clean APIs, validation, caching, and more—with real-world patterns in JavaScript and TypeScript.

Introduction

Most devs think of object properties as just “bags of values”:

const user = { name: "Ali" };
console.log(user.name); // "Ali"

But JavaScript lets you define properties that run code when you access or assign them. These are getters and setters.

They’re powerful for things like:

  • Computed values (fullName from firstName + lastName)
  • Validation (reject invalid assignments)
  • Lazy evaluation (compute once, cache forever)
  • Deprecation shims (warn when old fields are used)
  • Encapsulation (control how data is exposed)

In this post, you’ll learn how to use them effectively, common mistakes, and practical recipes you can use in real-world apps.


1. Declaring Getters and Setters

The syntax is built right into object literals and classes:

const user = {
firstName: "Ali",
lastName: "Khan",
get fullName() {
return `${this.firstName} ${this.lastName}`;
},
set fullName(value) {
const [first, ...rest] = value.split(" ");
this.firstName = first;
this.lastName = rest.join(" ");
}
};

console.log(user.fullName); // "Ali Khan"
user.fullName = "Laiba Rahmat";
console.log(user.firstName); // "Laiba"
  • Getter (get) runs when you read the property.
  • Setter (set) runs when you assign to the property.

2. Defining with Object.defineProperty

For finer control over descriptors:

const account = {};
let _balance = 0;

Object.defineProperty(account, "balance", {
get() {
return _balance;
},
set(v) {
if (v < 0) throw new Error("Balance cannot be negative");
_balance = v;
},
enumerable: true,
configurable: true
});

account.balance = 100;
console.log(account.balance); // 100

👉 Useful when you want to hide backing fields or make them non-enumerable.


3. Real-World Use Cases

A) Computed Properties

const rect = {
width: 10,
height: 5,
get area() {
return this.width * this.height;
}
};

console.log(rect.area); // 50

No need to manually recalc — area is always derived from current state.


B) Validation on Assignment

const user = {
_age: 0,
get age() {
return this._age;
},
set age(value) {
if (value < 0) throw new Error("Age must be positive");
this._age = value;
}
};

user.age = 25; // ok
// user.age = -5; // throws

C) Encapsulation & Hidden State

class Counter {
#count = 0; // private field (ES2022+)

get value() {
return this.#count;
}
set value(v) {
if (!Number.isInteger(v)) throw new Error("Must be integer");
this.#count = v;
}
}
const c = new Counter();
c.value = 5;
console.log(c.value); // 5

D) Lazy Evaluation + Caching

const data = {
get expensive() {
console.log("computing...");
const value = Math.random();
Object.defineProperty(this, "expensive", { value, writable: false });
return value;
}
};

console.log(data.expensive); // logs "computing..." then number
console.log(data.expensive); // cached number, no recompute

E) Deprecation Shims

const settings = {
theme: "dark",
get colour() {
console.warn("⚠️ 'colour' is deprecated, use 'theme' instead.");
return this.theme;
},
set colour(v) {
console.warn("⚠️ 'colour' is deprecated, use 'theme' instead.");
this.theme = v;
}
};

settings.colour = "light"; // warning
console.log(settings.theme); // "light"

4. Getters/Setters in Classes

Widely used in OOP-style JavaScript (e.g., with TypeScript).

class User {
constructor(private _email: string) {}

get email() {
return this._email.toLowerCase();
}

set email(v: string) {
if (!v.includes("@")) throw new Error("Invalid email");
this._email = v;
}
}

const u = new User("Test@Example.com");
console.log(u.email); // "test@example.com"
u.email = "new@example.com"; // ok

👉 TypeScript automatically infers types for get/set pairs.


5. Integration with JSON, Spread, and Keys

  • Enumerable matters: by default, getters/setters are enumerable in object literals.
  • Non-enumerable props won’t appear in:
  • Object.keys(obj)
  • for...in loops
  • JSON.stringify(obj)
const obj = {
get hidden() { return 42; }
};

console.log(Object.keys(obj)); // []
console.log(JSON.stringify(obj)); // {}

👉 If you want getters in JSON, return them explicitly in .toJSON():

const user = {
first: "Ali",
last: "Rahmat",
get fullName() {
return `${this.first} ${this.last}`;
},
toJSON() {
return { fullName: this.fullName };
}
};

console.log(JSON.stringify(user)); // {"fullName":"Ali Rahmat"}

6. Gotchas to Watch Out For

  1. Don’t abuse for heavy computation
  • Getters should feel cheap. If expensive, cache the result.

2. Avoid side effects in getters

  • A get should not cause mutations or network calls. Treat them like pure functions.

3. One descriptor = either data or accessor

  • You can’t have value + get in the same property.

4. Don’t forget enumerable/configurable flags

  • Especially when using Object.defineProperty, defaults are false.

5. Beware infinite recursion

const obj = {
get value() {
return this.value; // ❌ calls itself forever
}
};

7. Handy Utility Patterns

Freeze computed values once

function defineCached(obj, key, compute) {
Object.defineProperty(obj, key, {
get() {
const val = compute();
Object.defineProperty(obj, key, { value: val });
return val;
},
configurable: true
});
}

Define multiple props at once

Object.defineProperties(user, {
fullName: {
get() { return `${this.first} ${this.last}`; },
enumerable: true
},
initials: {
get() { return `${this.first[0]}${this.last[0]}`; },
enumerable: true
}
});

8. Performance Considerations

  • Getters/setters are slightly slower than direct property access, but negligible unless in hot loops.
  • Use caching for expensive calculations.
  • For interception-heavy logic (like observing every property), use a Proxy instead.

Conclusion

Getters and setters are a powerful middle ground between plain objects and full-blown classes:

  • Getters: computed, derived, or lazy properties.
  • Setters: validate or transform inputs.
  • ✅ Work seamlessly in objects, classes, and defineProperty.
  • ⚠️ Don’t abuse for side effects or expensive work — keep them predictable.

Pro Tip: Think of them as controlled views of data: getters expose data in a safe, readable way; setters protect your object from invalid state.


Call to Action (CTA)

Have you used getters/setters for something clever — like caching or API wrappers?
Share your snippet in the comments, and pass this article to a teammate who still thinks get/set are “just syntactic sugar.”

Leave a Reply

Your email address will not be published. Required fields are marked *