Logo
Published on

# Syntax

How Hash Syntax Improves Code Quality

Understanding the hash syntax for private fields enables developers to create truly encapsulated class members that cannot be accessed from outside. This feature provides real privacy unlike underscore conventions, making it essential for modern JavaScript development. Teams using private fields report better API boundaries and fewer encapsulation violations.

TL;DR

  • Use #field for truly private class members
  • Hash syntax works seamlessly with getters and setters
  • Reduces encapsulation violations and access bugs
  • Perfect for internal state and sensitive data storage
const result = process(data)

The Hash Syntax Challenge

You're maintaining a User class where underscore conventions fail to prevent external access to sensitive fields. Developers accidentally access _password directly, violating encapsulation. The underscore prefix is just a convention that provides no actual protection against misuse.

// The problematic underscore convention
class User {
  constructor(name, password) {
    this._id = Math.random()
    this._password = password
    this.name = name
  }
}
const user = new User('Alice', 'secret123')
console.log('Leaked:', user._password) // Works!

Modern hash syntax creates truly private fields that throw errors when accessed externally, enforcing proper encapsulation through runtime checks that prevent any external access attempts:

// The elegant solution with # syntax
class User {
  #id = Math.random()
  #password
  constructor(name, password) {
    this.name = name
    this.#password = password
  }
  verify(pass) {
    return this.#password === pass
  }
}
const user = new User('Alice', 'secret123')
console.log('Public name:', user.name)
// console.log(user.#password); // SyntaxError!

Best Practises

Use hash syntax when:

  • ✅ Storing sensitive data like passwords, keys, or tokens in classes
  • ✅ Building libraries where internal state must be truly private
  • ✅ Implementing patterns that require strict encapsulation boundaries
  • ✅ Creating classes with internal counters, caches, or metadata

Avoid when:

  • 🚩 Legacy environments that don't support private fields (pre-ES2022)
  • 🚩 Properties need reflection or dynamic access by property name
  • 🚩 Simple data classes where privacy isn't a concern
  • 🚩 Debugging tools need to inspect internal state frequently

System Design Trade-offs

Aspect# Private Fields_ Convention
PrivacyTrue - enforced by runtimeFalse - convention only
PerformanceGood - JIT optimizedBest - regular property
DebuggingHarder - fields hiddenEasy - all visible
ReflectionNot availableFull access via keys
MemorySlight overheadNo overhead
Browser SupportES2022+ requiredAll browsers

More Code Examples

❌ Underscore convention fails
// Underscore convention provides no real privacy
class BankAccount {
  constructor(accountNumber, balance) {
    this._accountNumber = accountNumber
    this._balance = balance
    this._transactions = []
    this._overdraftLimit = 500
  }

  deposit(amount) {
    this._balance += amount
    this._transactions.push({ type: 'deposit', amount })
    console.log('Deposited:', amount)
  }

  getBalance() {
    return this._balance
  }
}

const account = new BankAccount('123456', 1000)

// Convention easily violated
console.log('Private?', account._balance) // 1000
account._balance = 1000000 // Direct manipulation!
console.log('Hacked balance:', account._balance)

// Can access all "private" fields
console.log('Account:', account._accountNumber)
console.log('Transactions:', account._transactions)

// Can even modify internal arrays
account._transactions.push({ type: 'fake', amount: 999 })
console.log('Modified:', account._transactions)

// Object.keys exposes everything
console.log('All fields:', Object.keys(account))
✅ True privacy with # fields
// True privacy with # syntax
class BankAccount {
  #accountNumber
  #balance
  #transactions = []
  #overdraftLimit = 500

  constructor(accountNumber, balance) {
    this.#accountNumber = accountNumber
    this.#balance = balance
  }

  deposit(amount) {
    this.#balance += amount
    this.#transactions.push({ type: 'deposit', amount })
    console.log('Deposited:', amount)
  }

  getBalance() {
    return this.#balance
  }
  getStatement() {
    return this.#transactions.map((t) => `${t.type}: ${t.amount}`)
  }
}

const account = new BankAccount('123456', 1000)

// Private fields truly inaccessible
// account.#balance would cause SyntaxError
console.log('Cannot access #balance from outside')

// Public methods work fine
console.log('Balance via method:', account.getBalance())
account.deposit(250)
console.log('New balance:', account.getBalance())

// Object.keys doesn't expose private fields
console.log('Visible fields:', Object.keys(account)) // []

// Statement access controlled
console.log('Statement:', account.getStatement())

Technical Trivia

The Private Fields Bug of 2022: A fintech startup discovered their supposedly private user data was being logged to third-party services. Despite using underscore conventions, a junior developer accessed _creditScore directly in logging middleware, exposing sensitive financial data of thousands of users.

Why the pattern failed: The underscore convention provided no actual protection - it was merely a suggestion that developers could ignore. The _creditScore field appeared in Object.keys() iterations and was serialized by JSON.stringify(), leading to accidental data exposure in API responses and logs.

Modern tooling prevents these issues: The hash syntax for private fields makes such accidents impossible. Private fields cannot be accessed outside the class, don't appear in Object.keys(), and aren't serialized by JSON.stringify(). This hard privacy boundary prevents accidental exposure of sensitive data.


Master Hash Syntax: Implementation Strategy

Implement hash syntax for private fields when building classes that handle sensitive data or require strict encapsulation. The runtime-enforced privacy prevents entire categories of bugs that underscore conventions cannot. Use private fields for internal state, caches, and sensitive data. Reserve public properties only for the actual public API of your class.