Logo
Published on

Property Access

How Property Access Improves Code Quality

Understanding getters and setters for property access enables developers to intercept property reads and writes with custom logic. This technique provides encapsulation while maintaining simple property syntax, making it essential for modern JavaScript development. Teams using property accessors report better data validation and cleaner APIs.

TL;DR

  • Use get/set for controlled property access
  • Property Access works seamlessly like regular properties
  • Reduces direct property manipulation risks
  • Perfect for lazy loading and data validation
const result = process(data)

The Property Access Challenge

You're reviewing a class where properties are directly exposed, allowing any code to modify internal state without validation. The current implementation has no way to track changes, compute derived values, or ensure data consistency when properties are accessed or modified.

// The problematic direct property access
class BankAccount {
  constructor(balance) {
    this.balance = balance // Directly exposed!
    this.overdraftLimit = -500
  }
}
const account = new BankAccount(100)
account.balance = -9999 // No validation!
console.log('Broken:', account.balance)

Modern getters and setters provide controlled property access while maintaining simple property syntax:

// The elegant solution with getters/setters
class BankAccount {
  constructor(balance) {
    this._balance = balance
    this._overdraftLimit = -500
  }
  get balance() {
    return this._balance
  }
  set balance(val) {
    if (val < this._overdraftLimit) throw new Error('Overdraft')
    this._balance = val
  }
}
const account = new BankAccount(100)
console.log('Using getters/setters')

Best Practises

Use property access when:

  • ✅ Properties need validation or transformation on set
  • ✅ Computing derived values on property read
  • ✅ Implementing lazy loading or caching strategies
  • ✅ Creating observable properties that trigger updates

Avoid when:

  • 🚩 Simple data containers without behavior
  • 🚩 Performance-critical tight loops (getters add overhead)
  • 🚩 Properties that should be immutable (use readonly)
  • 🚩 Team expects plain object behavior

System Design Trade-offs

AspectGetters/SettersDirect Properties
EncapsulationExcellent - full controlNone - direct access
PerformanceSlower - function callsFastest - direct read
ValidationBuilt-in - in setterManual - external checks
DebuggingBreakpoints in accessorsProperty watch only
Computed ValuesEasy - in getterManual calculation
Browser SupportES5+ requiredAll browsers

More Code Examples

❌ Direct property chaos
// Direct property access without control
class Temperature {
  constructor() {
    this.celsius = 0
    this.fahrenheit = 32
    this.kelvin = 273.15
  }

  setCelsius(value) {
    this.celsius = value
    // Must manually update related properties
    this.fahrenheit = (value * 9) / 5 + 32
    this.kelvin = value + 273.15
  }

  setFahrenheit(value) {
    this.fahrenheit = value
    // Duplicate conversion logic
    this.celsius = ((value - 32) * 5) / 9
    this.kelvin = ((value - 32) * 5) / 9 + 273.15
  }
}

const temp = new Temperature()
temp.setCelsius(100)
console.log('C:', temp.celsius, 'F:', temp.fahrenheit)

// Direct manipulation breaks consistency
temp.celsius = 0 // Other properties not updated!
console.log('Broken F:', temp.fahrenheit) // Still 212!
console.log('Broken K:', temp.kelvin) // Still 373.15!

// No validation possible
temp.kelvin = -500 // Impossible temperature!
console.log('Invalid:', temp.kelvin)

// Manual method calls required
temp.setFahrenheit(32)
console.log('Manual update needed')

// Can't track property access
console.log('No way to know property was read')
✅ Controlled access magic
// Getters/setters provide controlled access
class Temperature {
  constructor() {
    this._celsius = 0
  }

  get celsius() {
    console.log('Reading celsius')
    return this._celsius
  }

  set celsius(value) {
    if (value < -273.15) {
      throw new Error('Below absolute zero!')
    }
    console.log('Setting celsius to', value)
    this._celsius = value
  }

  get fahrenheit() {
    return (this._celsius * 9) / 5 + 32
  }

  set fahrenheit(value) {
    this.celsius = ((value - 32) * 5) / 9
  }

  get kelvin() {
    return this._celsius + 273.15
  }

  set kelvin(value) {
    this.celsius = value - 273.15
  }
}

const temp = new Temperature()

// Looks like property access, runs setter
temp.celsius = 100
console.log('C:', temp.celsius, 'F:', temp.fahrenheit)

// Automatic conversion
temp.fahrenheit = 32
console.log('Auto update C:', temp.celsius)
console.log('Auto update K:', temp.kelvin)

// Validation in setter
try {
  temp.kelvin = -500 // Throws error!
} catch (e) {
  console.log('Validation:', e.message)
}

// Computed properties always correct
temp.celsius = 25
console.log('All synced:', temp.celsius, temp.fahrenheit, temp.kelvin)

// Can track access in getters
const reading = temp.celsius // Logs "Reading celsius"

Technical Trivia

The Property Access Bug of 2019: A cryptocurrency exchange lost funds when direct property manipulation bypassed balance validation. Attackers discovered they could set account.balance directly in the API, bypassing the withdraw() method's checks, allowing negative balances and unlimited withdrawals.

Why the pattern failed: The Account class exposed balance as a public property without getters/setters. While the withdraw() method validated transactions, direct property assignment account.balance -= amount bypassed all checks. The missing encapsulation allowed state manipulation that violated business rules.

Modern tooling prevents these issues: Today's getters and setters enforce validation on every property access. TypeScript's private fields and accessor decorators provide compile-time safety. Modern frameworks like MobX and Vue use getters/setters for reactive state management.


Master Property Access: Implementation Strategy

Implement getters and setters when properties need validation, computation, or observation. The encapsulation benefits and API consistency outweigh the minor performance overhead. Use them for domain objects with business rules, but keep simple data transfer objects as plain properties. Remember that getters/setters look like properties but act like methods.