Stage 1
Classification: Syntactic Change Semantic Change
Human Validated: KW
Title: Private declarations
Authors: Justin Ridgewell
Champions: Justin Ridgewell
Last Presented: March 2019
Stage Upgrades:
Stage 1: 2019-04-08
Stage 2: NA
Stage 2.7: NA
Stage 3: NA
Stage 4: NA
Last Commit: 2020-01-21
Topics: objects others
Keywords: protected private class visibility
GitHub Link: https://github.com/tc39/proposal-private-declarations
GitHub Note Link: https://github.com/tc39/notes/blob/HEAD/meetings/2019-03/mar-28.md#private-declarations-for-stage-1

Proposal Description:

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);
}