Private Constructors

There have been many times where I've wanted to expose multiple "constructors" through static factory methods, and have the default constructor only be used internally by the factories.

As a concrete example:

class Counter {
  #nextNumb
  // This should not get used directly.
  constructor({ nextNumb = 0 } = {}) {
    this.#nextNumb = nextNumb
  }

  static create = () => new Counter()
  countUp = () => ++this.#nextNumb
  clone = () => new Counter({ nextNumb: this.#nextNumb })
}

Ideally, the constructor shouldn't be used from outside this class, but there's no easy way to self-document that.

I'm proposing that javascript adds the ability to have private constructors, like this:

#constructor({ nextNumb = 0 } = {}) { ... }

Only internal methods would have the ability to create instances of this class. Outside users have to instantiate it through one of the static factory methods.

I realize there's some technecal hurdles that need to be overcome in order to make this possible. The biggest issue is that in javascript, the constructor is the class, so there isn't an easy way to just prevent access to the constructor from the outside world.

The following solution should solve this issue: Like with other properties, both a public and a private constructor can exist simultaneously. The public constructor gets invoked with the new operator, while the private constructor gets invoked with a new "new#" operator. More specifically, new# will use MyClass.prototype.#constructor as the constructor. If a private constructor is defined, and a public one is omitted, then the default public constructor will simply be a function that throws an error. If a private constructor calls super(), it'll call the parent class's public constructor.

Here's a concrete example. Because a private constructor is present, and a public constructor is not provided, the public constructor defaults to a function that throws an error.

class Counter {
  #nextNumb
  #constructor({ nextNumb = 0 } = {}) {
    this.#nextNumb = nextNumb
  }

  static create = () => new# Counter()
  countUp = () => ++this.#nextNumb
  clone = () => new# Counter({ nextNumb: this.#nextNumb })
}

new Counter() // This default constructor throws an error.
new# Counter() // Doesn't work - can't access a private constructor outside of its class.
Counter.create() // returns a new instance.

An alternative version, with both a public and a private constructor

class Counter {
  #nextNumb
  #constructor({ nextNumb }) {
    this.#nextNumb = nextNumb
  }
  constructor() {
    this.#nextNumb = 0
  }

  countUp = () => ++this.#nextNumb
  clone = () => new# Counter({ nextNumb: this.#nextNumb })
}

new Counter() // Returns a new instance
new# Counter() // Doesn't work - can't access a private constructor outside of its class.

Thoughts? Is such a feature valuable enough to introduce a new operator? Are there other, better ways to achieve this functionality?

One alternative would be to have "internal-use-only" constructors instead of private constructors. The objective is the same, but the implementation is different. The #constructor() {} syntax will not create a private constructor (meaning it'll still place this function on MyClass.prototype.constructor, not MyClass.prototype.#constructor) - instead, it sets some internal field on the constructor, specifying that it can only be used from within the class. When the new keyword is used on one of these marked constructors, an error will be thrown, unless "new" is used within the same class. This changes what "#" means for this specific situation, but it might be a more intuitive meaning, as this implementation would be closer to how private constructors are implemented in other languages.

1 Like

This sounds vaguely similar the pattern I've used for this in the past.

// module scope
const secret = {}
class PrivateConstructor {
  constructor (key) {
    if (key !== secret) 
      throw TypeError('PrivateConstructor is not a constructor')
  }

  static create () {
    return new PrivateConstructor(secret)
  }
}

// some other scope
const a = PrivateConstructor.create() // OK
const b = new PrivateConstructor() // TypeError

That said, I've rarely had need to enforce private constructors personally, and don't find this particular pattern too difficult to implement if needed.

We actually put in a SyntaxError in the private fields/methods proposals to reserve #constructor() {} for exactly this use case.

2 Likes

I've never thought of that technique for creating private constructors. It is pretty ugly though ... for internal classes, I would rather just put a comment on the constructor saying "This constructor is private, please use one of the static factory methods".

But, that's a good trick to know.

Another use case for private constructors is when you want your constructor to perform async tasks.

A normal constructor can't be made async. Instead, async logic can be put into a static factory on the class, and that factory method can then call a private constructor.

class User {
  #constructor({ name, age }) {
    this.name = name
    this.age = age
  }

  static async create(id) {
    return new User(await fetchUserData(id))
  }
}

The way I've handled that, the one or two times I've needed it, is:

const sentinel = {};

class User {
constructor(secret, { name, age }) {
if (secret !== sentinel) {
throw new TypeError('private constructor');
}
[this.name](http://this.name) = name
this.age = age
}

static async create(id) {
return new User(sentinel, await fetchUserData(id))
}
}

One Problem I see with the MyClass.prototype.#constructor approach is that .constructor is just a regular property. The new operator calls the function passed as it's operand, not the prototype's constructor property. So while new and new# look very similar, their behavior is quite different. Also it quite breaks the "a class is just a function" concept. I'd prefer having just one constructor, a private one replacing the public one, that throws if it gets called with new from the outside (similar to the snippets above). Detecting from where the constructor was called could be made available through a Metaproperty just like new.target, e.g. new.self which is set if the function new is called on is the [[HomeObject]] of the function currently executing. Polyfilling would then also be quite easy:

    const _new = { self: false };

    class MyClass {
      constructor() {
        if(!_new.self) 
          throw new Error("MyClass has a private constructor and can only be constructed from within it's methods"); 
         _new.self = false;
          /*...*/
       }

       static create() {
         _new.self = true;
        new MyClass();
      }
    }

I think that would align quite nicely with the existing language and wouldn't be "too much magic". Also with this approach there is not yet another keyword (new#).

1 Like