For an API I'm writing I want to make sure the values I pass around can only be instantiated by my own specific functions to ensure the validity of said values. I know the tried and true method is to create a class with a private constructor and static factory methods like the example below (hypothetical situation; the actual class is more complex and has methods).
export class ServiceEndpoint {
private constructor(
readonly name: string,
readonly address: string,
readonly port: number
) {}
static getByName(name: string): ServiceEndpoint {
// ... polling operation that results in an address
return new this(name, address, port);
}
}
But in order to allow for a more functional approach I would like the factory function to live outside of the class itself (but in the same module) in order to be able to more freely compose different variants. I came up with the following approach, where I keep the constructor public but restrict it to functions in the same module by not exporting it:
class ServiceEndpoint {
constructor(
readonly name: string,
readonly address: string,
readonly port: number
) {}
}
export function getEndpointByName(name: string): ServiceEndpoint {
// ... polling operation that results in an address
return new ServiceEndpoint(name, address, port);
}
Testing this seems to yield the same result, but I haven't seen other people do it like this so I'm a bit cautious. Am I rightfully assuming this prevents users of the module to instantiate the class on their own, just like a private constructor does? Are there disadvantages to this method I am overseeing?
I was intrigued by your question and I did some tests to try answering it in the best way possible.
First thing, remember that TypeScript is not JavaScript and it will be ultimately compiled before being ran. Writing
privatebefore declaring the constructor will have no particular effect on compiled code, in other words, not a bit of increased 'security' for doing that.As @Bergi correctly stated in the comments, generally you can always access a constructor through an instance
constructorproperty and potentially doconst illegalInstance = new legalInstance.constructor();This last scenario can be avoided by removing
constructorreference completely. Something like:To more specifically address your concerns, after removing
constructorreference, not exporting your class is sufficient to assume no illegal instances will be created outside of that module. (However, I would never rely on anything in memory for security critical scenarios).Finally, you could perform some checks inside your constructor, and throw errors if those checks do not pass. This will prevent the class from being instantiated.
Remember that
classsyntax is just syntactic sugar for constructor functions. At the end, the only thing that matters is what ends in the resulting object and its prototype.Ah! And don't worry about
export type {ServiceEndpoint};, again, this is not JavaScript and will be removed at compile time.