DEV Community

Tyler Smith
Tyler Smith

Posted on

Faking private class constructors in JavaScript

Private class constructors can be helpful, but unfortunately JavaScript doesn't support them. The Mozilla Developer Network site suggests simulating private constructors by mutating static properties on the base class, but the implementation feels hacky.

Faking private constructors

You can simulate a private class constructor in JavaScript by requiring the constructor to accept something that is unavailable to the rest of the code. We can use an immediately invoked function expression to do just that.

const PrivateConstructor = (function () {
  class PrivateParams {
    constructor(param1, param2) {
      this.param1 = param1;
      this.param2 = param2;
    }
  }

  return class PrivateConstructor {
    constructor(privateParams) {
      if (!(privateParams instanceof PrivateParams)) {
        throw new TypeError(
          "PrivateConstructor is not constructable. " +
          "Use PrivateConstructor.make()."
        );
      }

      this.param1 = privateParams.param1;
      this.param2 = privateParams.param2;
    }

    static make(param1, param2) {
      return new PrivateConstructor(
       new PrivateParams(param1, param2)
      );
    }
  };
})();

// Example usage:
PrivateConstructor.make(1, 2);

// Throws error:
new PrivateConstructor(1, 2);
Enter fullscreen mode Exit fullscreen mode

How it works

The code above executes an immediately invoked function expression, and it returns the PrivateConstructor class that is defined within its scope. The constructor method requires a PrivateParams instance, and if it receives anything else it will throw an error. The PrivateParams class is only available within the immediately invoked function expression: nothing outside of its scope can instantiate the class.

This leaves only one way to instantiate the class: the static .make() method, which passes in a PrivateParams instance, satisfying the constructor and successfully returning an instantiated instance of the PrivateConstructor class.

Other approaches

Another option to restrict a class's instantiation could be to use a symbol that is only available within the scope of the class as an argument to the constructor. Symbols are guaranteed to be unique by the language, so it couldn't be forged.

const PrivateConstructor = (function () {
  const _instantiationToken = Symbol();

  return class PrivateConstructor {
    constructor(param1, param2, instantiationToken) {
      if (instantiationToken !== _instantiationToken) {
        throw new TypeError(
          "PrivateConstructor is not constructable. " +
          "Use PrivateConstructor.make()."
        );
      }

      this.param1 = param1;
      this.param2 = param2;
    }

    static make(param1, param2) {
      return new PrivateConstructor(
        param1,
        param2,
        _instantiationToken
      );
    }
  };
})();

// Example usage:
PrivateConstructor.make(1, 2);

// Throws error:
new PrivateConstructor(1, 2);
Enter fullscreen mode Exit fullscreen mode

While less code than the first example, having a separate parameter for an access token feels hacky, and somehow feels like an even leakier abstraction that we're trying to shore up shortcomings in JS itself.

Should you do this?

Probably not. Certainly don't do this in application code if you can avoid it. Both of these examples are clever code, and committing clever code to a project is one of the leading causes of coworkers hating you.

Clever code like this may be appropriate in a library or framework, but be careful before you implement it in your own project.

Honorable mention: TypeScript

I should mention that unlike JavaScript, TypeScript does have this capability. Here's what that looks like:

class PrivateConstructor {
  param1: number;
  param2: number;

  private constructor(param1: number, param2: number) {
    this.param1 = param1;
    this.param2 = param2;
  }

  static make(param1: number, param2: number) {
    return new PrivateConstructor(param1, param2,);
  }
};


// Example usage:
PrivateConstructor.make(1, 2);

// Throws compiler error:
new PrivateConstructor(1, 2);
Enter fullscreen mode Exit fullscreen mode

This is pretty nice: it avoids the clever code problem and limits external access to the constructor. Unfortunately your project might now be in TypeScript. You can't win them all.

Top comments (0)