For AI agents: the complete documentation index is available at /tc39-atlas/en/llms.txt, the full documentation bundle is available at /tc39-atlas/en/llms-full.txt, and this page is available as Markdown at /tc39-atlas/en/proposals/stage/1/proposal-private-declarations.md.
  • English
  • Private declarations S1

    Proposal details
    Proposal overview

    The proposal adds private declarations, allowing trusted code outside a class lexical scope to access private state. It also allows private state to be added to regular objects. The syntax uses private #name; declarations, and private fields can then be accessed with the usual #name syntax. Possible later proposals could allow sharing private declarations with friendly modules.

    Note

    The README below comes from the upstream repository and may contain outdated stage or status metadata. Use the proposal details above as the current source of truth.

    Private Declarations

    A proposal to add Private Declarations, allowing trusted code outside of the class lexical scope to access private state.

    private #hello;
    class Example {
      #hello = 'world!';
    
      hello() {
        return this.#hello;
      }
    }
    
    const ex = new Example();
    console.log(ex.hello()); // => 'world!'
    console.log(ex.#hello); // => 'world!'

    This also allows us to bring private state to regular objects!

    private #hello;
    function Example() {
      return {
        #hello: 'world',
    
        hello() {
          return this.#hello;
        }
      }
    }
    
    const ex = Example();
    console.log(ex.hello()); // => 'world!'
    console.log(ex.#hello); // => 'world!'

    Possible later proposals can allow sharing private declarations to friendly module.

    Champions

    Status

    Current Stage: 1

    Guiding Use Cases

    "Protected" state

    Protected state is a valuable visibility state when implementing class hierarchies. For for instance, a hook pattern might be used to allow subclasses to override code:

    // https://github.com/Polymer/lit-html/blob/1a51eb54/src/lib/parts.ts
    
    private #createPart;
    
    class AttributeCommitter {
      //...
    
      #createPart() {
        return new AttributePart(this);
      }
    }
    
    class PropertyCommitter extends AttributeCommitter {
      #createPart() {
        return new PropertyPart(this);
      }
    }

    Here, AttributeCommitter explicitly allows trusted (written in the same source file) subclasses to override the behavior of the #createPart method. By default, a normal AttributePart is returned. But PropertyCommitter works only on properties and would return a PropertyPart. All other code is free to be inherited via normal publicly visible fields/methods from AttributeCommitter.

    Note that this does not privilege code outside the file to override the #createPart method, as the #createPart private declaration is visible only in the scope where it is declared.

    Friend classes/functions

    For prior art in C++, see https://en.wikipedia.org/wiki/Friend_function.

    The AMP Project has a particular staged linting pattern that works well with friendly functions that guard access to restricted code. To begin with, statically used functions are considerably easier to lint for than object-scoped method calls because we do not need to know the objects type. So its much easier to determine if this is a restricted call or just a non-restricted call that uses the same method name. Eg, it's easier to tell that a static export registerExtendedTemplate is restricted vs obj.registerExtendedTemplate.

    // https://github.com/ampproject/amphtml/blob/18baa9da/src/service/template-impl.js
    
    private #registerTemplate;
    
    // Exported so that it may be intalled on the global and shared
    // across split bundles.
    export class Templates {
      #registerTemplate() {
        //...
      }
    }
    
    // The code privileged to register templates with the shared class
    // instance. Importing and using is statically analyzable, and must pass
    // a linter.
    export function registerExtendedTemplate() {
      const templatesService = getService('templates');
      return templatesService.#registerTemplate(...arguments);
    }

    Non-class object factories

    Not all code wants a class. Sometimes you still want private fields in such cases.

    private #hash;
    
    export function factory(value) {
      return Object.freeze({
        value,
        #hash: someFunction(value),
      });
    }
    
    export function isProducedByMyFactory(value) {
      return #hash in value;
    }
    
    export function equals(a, b) {
      if (a.#hash !== b.#hash) return false;
      return expensiveCompare(a.value, b.value);
    }