JDB
JDB

Reputation: 25810

How to extend a class wrapped in a Proxy

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:

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

Upvotes: 1

Views: 1463

Answers (2)

Bergi
Bergi

Reputation: 664484

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.

Upvotes: 0

Bergi
Bergi

Reputation: 664484

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"?.

Upvotes: 5

Related Questions