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