1

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?

soimon
  • 2,400
  • 1
  • 15
  • 16
  • 2
    In ES6 you can always do `const someEndpoint = getEndPointByName(…); const myEndpoint = new (someEndpoint.constructor)(…);`. However I guess TypeScript will complain if you made the constructor `private`. – Bergi Jan 07 '21 at 18:06
  • in the latter case, class `ServiceEndpoint` is not exposed outside of this module, is that what you want? – ABOS Jan 07 '21 at 18:09
  • I want the `ServiceEndpoint` to only be accessible as a returned value. In both cases I could `export type {ServiceEndpoint};` to access the type for use in declarations (I assume: that may be part of the gotcha) – soimon Jan 07 '21 at 18:15

2 Answers2

1

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.

Ernesto Stifano
  • 3,027
  • 1
  • 10
  • 20
  • See also [How to define private constructors in javascript?](https://stackoverflow.com/a/21731713/1048572) for a similar approach that still provides a constructor for use with `instanceof` – Bergi Jan 07 '21 at 20:54
-1

One approach I can think of is check a module-wise/private secret before instantiation, something like the following

// module -- ServiceEndpoint.ts   
const secret = Symbol('secret'); // do not export it so that it can be kept "private"

class ServiceEndpoint {
    // similar to class decorator ...
    static _secret: Symbol | undefined;
    constructor(
        readonly name: string,
        readonly address: string,
        readonly port: number
    ) {
        if(ServiceEndpoint._secret !== secret) {
            throw 'you can only instantiate this class using factory method'
        } else {
          // normal instantiation ....
        }
    }
}

export type { ServiceEndpoint} 

export function getEndpointByName(name: string): ServiceEndpoint {
    ServiceEndpoint._secret = secret;
    // ... polling operation that results in an address
    const instance =  new ServiceEndpoint(name, '', 0);
    ServiceEndpoint._secret = undefined;
    return instance;
}

// -----------------------------------------------------------------

// run example.ts
// outside the above module, you can call
getEndpointByName('haha');
// but the following will fail, i.e. throw exception 
new ServiceEndpoint('', '', 1);

Plus, if you use constructor of this instance, it will fail as well.

ABOS
  • 3,723
  • 3
  • 17
  • 23
  • You should not communicate by storing the secret as a static property on the class though. Worst case, that's just how you leak it. Instead, pass the secret as an additional constructor argument. – Bergi Jan 07 '21 at 20:14
  • I can access the secret by doing `Object.defineProperty(ServiceEndpoint, "_secret",{set(value) { console.log(secret, "Thanks!"); }}); getEndpointByName('haha');` (or variations of that). That it would be visible in the constructor signature does not matter, since the constructor itself is not publicably visible. If you insist on using a flag to communicate (which is a fragile approach, dealing badly with reentrancy, callbacks or exceptions in any case), at least don't make it a public static property! You have a perfectly good module scope. – Bergi Jan 07 '21 at 20:26
  • Ok, but even if you don't care about security it's still an overcomplicated and fragile approach. Use a simple `let callFromFactory = false;` boolean flag. – Bergi Jan 07 '21 at 20:34
  • Who cares whether `false` is "private" or not if `let callFromFactory` is private? It would be defined in the same scope as your `const secret`. – Bergi Jan 07 '21 at 20:41