1

I have a complex class that requires certain arguments to be passed to the constructor. However, I am exposing a simplified API to customers.

My internal class looks something like this:

class Foo {
  constructor(p, c) {
  }

  foo() {
  }
}

Where p is an internal reference that isn't conveniently accessible to customers.

Supporting a Public API

I want to allow customers to create instances of this class, but I don't want them to need a reference to the private p object. For consumers of this API, accessing p would be laborious and break existing code, so I want to hide it with an alias.

Sub-classes to the rescue? Almost.

At first I simply extended Foo, hid the private argument (by supplying the code to access it), and exposed it via the public API:

class PublicFoo extends Foo {
  constructor(c) {
    // Use internal functions to get "p"
    var p;
    super(p, c);
  }
}

This very nearly worked, but I ran into a major flaw. There are situations where the customer will need to test the type of an object. Depending on the situation, Foo might be created internally using the internal class or by the customer using the public API.

If the public API was used to create an instance of Foo, then internal instaceof checks work just fine: publicFoo instanceof Foo returns true. But, if the API created an instance of Foo using the internal class, then public instanceof checks fail: internalFoo instanceof PublicFoo returns false. The customer can type check instances created using the public API, but the same type checks fail for instances created internally (for example, by factory functions).

This is to be expected and makes sense to me, but it breaks my use-case. I can't use a simple sub-class because the sub-class is not a reliable alias for the internal class.

var f = new Foo();
f instanceof PublicFoo; // false

What about a Proxy?

So I bumped the "clever" gear up a notch and tried using a Proxy instead, which seemed(famous last words) to be a perfect solution:

var PublicFoo = new Proxy(Foo, {
  construct(target, args) {
    // Use internal functions to get "p"
    var p;
    return new target(p, ...args);
  }
});

I can expose the Proxy, intercept calls to the constructor, provide the necessary private object reference and instanceof isn't broken!

var f = new Foo();
f instanceof PublicFoo; // true!!!

But Proxy breaks inheritance...

Disaster! Customers can no longer inherit from (their version of) Foo!

class Bar extends PublicFoo {
  constructor(c) {
    super(c);
  }

  bar() {
  }
}

The Proxy's constructor trap always returns a new instance of Foo, not the subclass Bar.

This leads to terrible, terrible issues like:

(new Bar()) instanceof Bar; // false!!!! 

and

var b = new Bar();
b.bar() // Uncaught TypeError: b.bar is not a function

I'm Stuck.

Is there any way to meet all of the following criteria:

  • My public API must implicitly provide the "private" constructor arg (but, for reasons™, I can't do that in the Foo constructor itself... it must be done via a wrapper or intercept of some sort)
  • The public API's version of Foo should be a thin alias of Foo. So, given f = new Foo(), then f instanceof PublicFoo should return true
  • PublicFoo should still support extends. So given class Bar extends PublicFoo, then (new Bar()) instanceof Bar should return true.

Here's an interactive demonstration of my Proxy conundrum:

class Foo {
  constructor(p, c) {
    this.p = p;
    this.c = c;
  }
  
  foo() {
    return "foo";
  }
}

var PublicFoo = new Proxy(Foo, {
  construct(target, args) {
    var p = "private";
    return new target(p, ...args);
  }
});

var foo = new Foo("private", "public");
console.assert( foo instanceof PublicFoo, "Foo instances are also instances of PublicFoo" );
console.assert( foo.p === "private" );
console.assert( foo.c === "public" );

var publicFoo = new PublicFoo("public");
console.assert( publicFoo instanceof Foo, "PublicFoo instances are also instances of Foo" );
console.assert( publicFoo.p === "private" );
console.assert( publicFoo.c === "public" );

class Bar extends PublicFoo {
  constructor(c) {
    super(c);
  }
  
  bar() {
    return "bar";
  }
}

var i = new Bar("public");
console.assert( i instanceof Bar, "new Bar() should return an instance of Bar" );
console.assert( i.p === "private" );
console.assert( i.c === "public" );
i.foo(); // "foo"
i.bar(); // Uncaught TypeError: i.bar is not a function
Community
  • 1
  • 1
JDB
  • 25,172
  • 5
  • 72
  • 123

2 Answers2

5

What about a Proxy? A proxy breaks inheritance as it always returns a new instance of Foo, not the subclass Bar.

That's because your proxy implementation always constructed a new target, not accounting for the newTarget parameter and passing it on to Reflect.construct:

const PublicFoo = new Proxy(Foo, {
  construct(Foo, args, newTarget) {
    const p = …; // Use internal functions to get "p"
    return Reflect.construct(Foo, [p, ...args], newTarget);
  }
});

See also What is "new.target"?.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
0

I would recommend not to use subclassing, or to try something clever with a proxy. Why not just do

class Foo {
  // private API - people can try but they won't come up with a valid `p`
  constructor(p, c) {
  }
  // public API
  static create(c, ...args) {
    const p = …; // Use internal functions to get "p"
    return new this(p, c, ...args);
  }
  // public API
  foo() {
  }
}

Sure, people won't use new Foo but rather Foo.create(), and their subclasses will have to pass-through the internal p.

If you absolutely need to support new PublicFoo syntax, I would recommend

function PublicFoo(c) {
  const p = …; // Use internal functions to get "p"
  return new Foo(p, c, ...args);
}
PublicFoo.prototype = Foo.prototype;

(and possibly Foo.prototype.constructor = PublicFoo, if that matters to you). This pattern still supports ES6 inheritance (class extends PublicFoo) just fine, and regarding instanceof the PublicFoo and Foo are totally equivalent.

See also this answer for more details.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • The `p` is actually supplied by a wrapper `createPublicApi` function, which might be called multiple times to create different "versions" of the public API. The `p` is, in essence, a reference to the API version. So I can't use a static factory function as the class won't know which version to use. However, your second solution is intriguing. I will investigate. – JDB Mar 31 '20 at 19:39
  • @JDBstillremembersMonica Do you actually need `(new (createPublicApi("1.0")) instanceof createPublicApi("2.0")`? That would look weird to me. If not, you could just put the entire internal `Foo` class inside the `createPublicApi` function and access `p` through closure, creating different internal implementations for each call. – Bergi Mar 31 '20 at 19:47
  • I've greatly simplified the code for the sake of the question. Actually, `p` is already a closure variable. My public API class is also defined within the closure, but the base class is defined in a module. Users of the API will always have code executing within the context of a closure (that they are unaware of), and thus they don't need to deal with `p`, but my module-scoped internal code does need to worry about it. Your other answer was exactly what I needed. – JDB Mar 31 '20 at 19:52
  • I mean that (even if you have a shared base class between the public API classes) your subclassing approach should have worked. The problem imo is more with "*Depending on the situation, Foo might be created internally using the internal class or by the customer using the public API.*" - what you should fix is to create those internally created instances using the right api version (from the closure), not the base class. – Bergi Mar 31 '20 at 20:01
  • in those cases, passing in the correct parameter is not enough. I have to actually use the Public API class directly (otherwise `instanceof` will fail). But the Public API class is used for the public API. My internal, module-scoped classes don't have ready access to it, nor do I really want them to as that would create some confusion. – JDB Mar 31 '20 at 20:03
  • So, to be clear, I have, say, 3 instances of the public API, each with its own `p`. The customer doesn't need to be aware of `p` because their code is always operating within the context of a public API instance (so I can determine `p` implicitly). Each public API exposes a simplified public class that omits the `p` from the constructor, but underneath all public APIs are implementing the same module-scoped class. The module-scoped class is not directly aware of the public APIs or their simplified class constructors, so a factory function wouldn't know what to pass in for `p`. – JDB Mar 31 '20 at 20:06
  • "*The module-scoped class is not directly aware of the public APIs*" - yes, and I think that is a problem, especially if the user made their own subclass but then your module creates an instance of the module-scoped base class. It should refer to `this.constructor` (or just `this` in static methods) instead of `Foo`. – Bergi Mar 31 '20 at 20:09
  • Thanks. It'd probably take a couple hours to fully explain the framework I'm working on and why this convoluted stuff is necessary (lot's of history involved), but this answer was extremely helpful and I really appreciate your willingness to provide it even without fully understanding why it was needed. – JDB Mar 31 '20 at 20:12