Getters, Setters & Proxy

intermediate getters setters Proxy Reflect property-descriptors

Sometimes we need more control over what happens when a property is read or written. Maybe we want to validate data before it’s stored, compute a value on the fly, or log every property access. JavaScript gives us three tools for this: getters/setters, Object.defineProperty, and Proxy.

Getters and Setters in Object Literals

A getter runs when we read a property. A setter runs when we assign to it. They look like regular properties from the outside, but they’re actually function calls under the hood.

const user = {
  firstName: "Manish",
  lastName: "Prajapati",
  get fullName() {
    return `${this.firstName} ${this.lastName}`; // computed on access
  },
  set fullName(value) {
    const [first, last] = value.split(" ");
    this.firstName = first;
    this.lastName = last;
  }
};

console.log(user.fullName);       // "Manish Prajapati" (calls getter)
user.fullName = "Tony Stark";     // calls setter
console.log(user.firstName);      // "Tony"

We access fullName like a normal property — no parentheses needed. That’s the whole point: it feels like data, but it’s actually logic.

Getters and Setters in Classes

Same idea, but inside a class:

class Temperature {
  #celsius; // private field

  constructor(celsius) {
    this.#celsius = celsius;
  }
  get fahrenheit() {
    return this.#celsius * 9 / 5 + 32;
  }
  set fahrenheit(f) {
    this.#celsius = (f - 32) * 5 / 9;
  }
}

const temp = new Temperature(100);
console.log(temp.fahrenheit);  // 212 (computed from celsius)
temp.fahrenheit = 32;          // sets celsius to 0

This is great for validation too — we can check values in the setter and throw if they’re invalid.

Object.defineProperty

For more control, Object.defineProperty lets us add getters/setters to existing objects and configure whether properties are enumerable, writable, etc.

const account = { _balance: 0 };

Object.defineProperty(account, "balance", {
  get() { return this._balance; },
  set(value) {
    if (value < 0) throw new Error("Balance can't be negative");
    this._balance = value;
  },
  enumerable: true
});

account.balance = 100;  // works
// account.balance = -50; // Error: Balance can't be negative

This is lower-level than class syntax, but useful when we need to add accessors dynamically or configure property descriptors.

Enter the Proxy

Proxy is a whole different level. It wraps an entire object and intercepts any operation on it — not just specific properties. Think of it as a security guard that sits between the outside world and our object.

const user = { name: "Manish", age: 25 };

const proxy = new Proxy(user, {
  get(target, prop) {
    console.log(`Reading ${prop}`);
    return target[prop];
  },
  set(target, prop, value) {
    console.log(`Setting ${prop} to ${value}`);
    target[prop] = value;
    return true; // must return true for success
  }
});

proxy.name;           // logs: Reading name → "Manish"
proxy.age = 26;       // logs: Setting age to 26

A Proxy takes two arguments: the target (the original object) and a handler (an object with “trap” functions that intercept operations).

Common Proxy Traps

Here are the traps we’ll use most often:

TrapInterceptsExample Use
getReading a propertyDefault values, logging
setWriting a propertyValidation, reactive updates
hasThe in operatorHide certain properties
deletePropertyThe delete operatorPrevent deletion
const schema = {
  name: "string",
  age: "number"
};

const validated = new Proxy({}, {
  set(target, prop, value) {
    if (schema[prop] && typeof value !== schema[prop]) {
      throw new TypeError(`${prop} must be a ${schema[prop]}`);
    }
    target[prop] = value;
    return true;
  }
});

validated.name = "Manish"; // works
validated.age = 25;        // works
// validated.age = "old";  // TypeError: age must be a number

The Reflect API

Reflect provides default behavior for all the proxy traps. Instead of doing target[prop] ourselves, we use Reflect.get(target, prop). This matters because Reflect methods handle edge cases (like getters with the right this) that manual access can miss.

const logged = new Proxy({}, {
  get(target, prop, receiver) {
    console.log(`Accessed: ${String(prop)}`);
    return Reflect.get(target, prop, receiver); // proper default behavior
  },
  set(target, prop, value, receiver) {
    console.log(`Set: ${String(prop)} = ${value}`);
    return Reflect.set(target, prop, value, receiver);
  }
});

The rule of thumb: inside proxy traps, use Reflect methods instead of direct object operations.

Practical Use Cases

Reactive objects — This is how Vue 3’s reactivity works. A Proxy detects when data changes and triggers re-renders.

Auto-populating defaults:

const withDefaults = new Proxy({}, {
  get(target, prop) {
    if (!(prop in target)) {
      target[prop] = []; // auto-create empty array for missing keys
    }
    return target[prop];
  }
});

withDefaults.users.push("Manish"); // no need to initialize users first
console.log(withDefaults.users);   // ["Manish"]

Negative array indexing:

const arr = new Proxy([10, 20, 30], {
  get(target, prop) {
    const index = Number(prop);
    if (index < 0) return target[target.length + index]; // arr[-1] = last element
    return Reflect.get(target, prop);
  }
});

console.log(arr[-1]); // 30
console.log(arr[-2]); // 20

Proxy vs Getters/Setters

Getters/SettersProxy
ScopeSpecific properties onlyAll properties, including ones that don’t exist yet
PerformanceFaster (no indirection layer)Slight overhead
Use caseComputed values, simple validationDynamic behavior, meta-programming
SyntaxBuilt into classes/objectsWraps an existing object

Use getters/setters when we know exactly which properties need special behavior. Use Proxy when we need to intercept everything dynamically.

In simple language, getters and setters let us run code when a specific property is read or written — great for validation and computed values. Proxy goes further and intercepts every operation on an entire object, which is what powers frameworks like Vue 3 under the hood.

References