Will we ever get private symbols? `Symbol.private()`

Why

I feel like they'd be more useful than private fields for scenarios outside of classes.

(@zenparsing)

Also if Symbol.private() ever came out, who would actually keep using Symbol()? I feel like most people would want to migrate to Symbol.private().

I feel that Symbols read nicer when associating state with objects:

const someValue = new WeakMap()

someValue.set(someObject, 123)
log( someValue.get(someObject) )

vs

const someValue = Symbol.private()

someObject[someValue] = 123
log( someObject[someValue] )

Besides the index access syntax is more useful:

someObject[someValue]++
someObject[someValue] += 123
// --
// *=
// etc

Symbols are also a smidge faster than WeakMap in V8 as of writing this:


Syntax sugar?

What would syntax sugar look like?

const someValue = Symbol.private()

someObject.!someValue = 123
log( someObject.!someValue )

For classes, could the sugar create the binding outside of the class automatically? F.e.

class SomeClass {
  !someValue = 123
}

const someValue = 123 // SyntaxError, someValue has already been declared.

or

const someValue = 123

class SomeClass {
  !someValue = 123 // SyntaxError, someValue has already been declared.
}

Basically this,

class SomeClass {
  !someValue = 123
}

would be effectively the same as

const someValue = Symbol.private('someValue') // name determined syntactically

class SomeClass {
  [someValue] = 123
}

And then, because the declared variable is in scope, it is usable anywhere in the class, as well as anywhere in the scope containing the class, matching the same semantics as without syntax sugar:

import {someFunction} from 'somewhere'

export class SomeClass {
  !someValue = 123

  someMethod() {
    console.log(this.!someValue)

    someFunction(this, someValue) // pass the private key to allow private access to certain API, etc
    anotherFunction(this)
  }
}

function anotherFunction(instance) {
  // This works! No limitation on syntactic location like #private fields!
  console.log(instance.!someValue)
}

Just as with any other undefined variable, a typo would result in a ReferenceError:

class SomeClass {
  !someValue = 123
}

const o = new SomeClass()

o.!someVale // ReferenceError: someVale is not defined

If we had this syntax, then maybe .!foo would just be short for [foo], and would work for any variable?

F.e.:

const prop = 'foo' // string property

o.!prop // it works

The class syntax !foo = 123 would be unique to classes for the field declarations, without the period.

For objects too?

const o = {
  // declares the variable in the scope that `o` is defined in.
  !someValue: 123
}

console.log( o.!someValue ) // 123

Encapsulate it, for example:

function foo() {
  return class {
    !someValue = 123

    method() {
      console.log( this.!someValue ) // 123
    }
  }
}

const someValue = 123 // ok, not declared in outer scope

const Class = foo()

const o = new Class()

console.log( o.!someValue ) // undefined because property "123" does not exist

Another option could be to make it module scoped:

function foo() {
  return class {
    !someValue = 123

    method() {
      console.log( this.!someValue ) // 123
    }
  }
}

const someValue = 123 // Error: someValue has already been declared

Perhaps in the immediate scope surrounding the class or object is better, more flexible, more options on how to organize? For example,

const someValue = Symbol.private()

function foo() {
  class Foo {
    [someValue] = 123
    !otherValue = 456

    method() {
      console.log( this.!someValue ) // 123
      console.log( this.!otherValue ) // 456
    }
  }

  const o = { !otherValue: 'blah' }
  console.log( o.!otherValue ) // "blah"

  return Foo
}

// here only `someValue` is visible outside the function
const o = new (foo())()
o.!someValue // 123
o.!otherValue // ReferenceError: otherValue is not defined

Also,

class Foo {
  static !someValue = 123

  get !foo() {...}
  set !foo() {...}

  !method() {...}

  accessor !bar = 456
}

console.log(someValue, foo, method, bar) // PrivateSymbol(someValue), PrivateSymbol(foo), PrivateSymbol(method), PrivateSymbol(bar)

Reasoning for the Sugar

The reasoning behind this syntax sugar idea, namely with class and object definitions, is to match a very similar semantic as we would have if we were using symbols without syntax sugar (with one difference: the scope in which the symbol is created is chosen for us).

If, for example, class { !someValue = 123 } did not expose the symbol in the outer scope, then it seems that there would be almost no practical difference between that and #private fields, and in that case it also seems that this.!someValue would essentially be syntax for inside the class/object definition just like #.

Would there be any difference that would be useful over private fields if the symbol were not to be exposed in outer scope? If only the internal implementation mechanic differed, but otherwise the DX for end developers was merely a different symbol (! vs #), it wouldn't seem worth having.

The advantage of exposing the symbol in the outer scope is that it can be used just like a symbol manually declared would be usable (share it with other code in different ways).

Advantages over private fields

Plus symbols have advantages over private fields, which if we had .! syntax would make them as easy to use syntactically as private fields, plus the optional benefits (for example, share a symbol to implement a loose protected methods that can be extended from class to subclass, not doable with pricate methods.

For example super.!someMethod() with a shared symbol would work just as with any regular property, the look up happening on the prototype chain.

There is also: GitHub - tc39/proposal-private-declarations: A proposal to allow trusted code _outside_ of the class lexical scope to access private state

Which has the advantage of being syntactic rather than needing the object methods to dynamically check if the symbol is private.

1 Like

I suppose there are pros and cons. Symbols can be passed around and be used in scenarios like string properties can be.

@aclaymore Private Declarations will not work with the class+field decorator workaround for avoiding accessor: Reactive/observable field without "accessor"? · Issue #478 · tc39/proposal-decorators · GitHub

With Private Symbols, we would be able to reference and store them, therefore making it possible to implement class field reactivity while avoiding the use of accessor (yay :tada:).

However the following proposal would allow for fields defined with Private Declarations to be interceptable: Intercept class field get/set (without accessor on prototype) · Issue #530 · tc39/proposal-decorators · GitHub

I would enjoy having fully private properties without having to start using accessor everywhere.