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/proposal-private-declarations.md.
  • English
  • Private declarations S1

    Proposal details
    Proposal overview

    This proposal introduces private declarations, allowing trusted code outside the class lexical scope to access private state. It supports protected state for subclasses, friend functions for controlled access, and private fields on non-class object factories.

    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 {
      outer #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 {
        outer #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 {
      //...
    
      outer #createPart() {
        return new AttributePart(this);
      }
    }
    
    class PropertyCommitter extends AttributeCommitter {
      outer #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 {
      outer #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,
        outer #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);
    }