Search code examples
typescriptecmascript-6private-constructor

Is not exporting your class as restrictive as using a private constructor?


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?


Solution

  • 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 private before 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 constructor property and potentially do const illegalInstance = new legalInstance.constructor();

    This last scenario can be avoided by removing constructor reference completely. Something like:

    class MyClass {
        constructor() {}
    }
    delete MyClass.prototype.constructor;
    
    export function myFactory() {
        return new MyClass();
    }
    

    To more specifically address your concerns, after removing constructor reference, 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.

    I haven't seen other people do it like this

    Remember that class syntax 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.