Henrique Guerra
Henrique Guerra

Reputation: 293

`Proxy` confusing `this[toString]` with `this[Symbol.toStringTag]`

It only happens with #toString, and only when I (try to) access it through a missingMethod-like trap.

I have a factory called createIterface which returns a Proxy of an object with a large number of methods. Among this methods, I have both #toString() and #id(). #id returns an interface with the same attributes as the caller and works just fine; #toString should convert my interface to a String, but it fails. All interface's methods - including #id and #toString - are inside a #Symbol.for("__methods") attribute. I have made it this way for debugging's purpouses:

const __methods = Symbol.for("__methods");

const missingMethod = ({
    get: (obj, prop) => Reflect.has(obj, prop)
        ? Reflect.get(obj, prop)
        : Reflect.has(obj[__methods], prop)
            ? Reflect.get(obj[__methods], prop)
            : console.log(`No #${prop} property exists.`)
});

const createInterface = (...props) => new Proxy({
    ...props,
    [__methods]: {
        id: () => createInterface (...props),
        toString: () => `Interface(${ props.toString() })`
    }
}, missingMethod);

const interface = createInterface(0, 1, 2);
interface.id(); //works
interface.toString(); //error: Cannot convert a Symbol value to a string

The error throwed says it cannot (implicitly) convert Symbol to String (which is true). Thing is, #toString is not a Symbol. There is, however, a well-known Symbol called #toStringTag that defines Object#toString() behavior. When I implement it with the other methods my #toString() is ignored and interface returns '[object Object]':

// see code above
const createInterface = (...props) => new Proxy({
    ...props,
    [__methods]: {
        id: () => createInterface (...props),
        toString: () => `Interface(${ props.toString() })`,
        [Symbol.toStringTag]: () => "Interface"
    }
}, missingMethod);

const interface = createInterface(0, 1, 2);
interface.id(); //works
interface.toString(); //bug: '[object Object]'

If I code the methods outside __methods it all works fine:

// see code above
const createInterface = (...props) => new Proxy({
    ...props,
    id: () => createInterface (...props),
    toString: () => `Interface(${ props.toString() })`
}, missingMethod);

const interface = createInterface(0, 1, 2);
const copycat = interface.id();
interface.toString() === copycat.toString(); //true

Other than some weird browse bug (I'm running latest Chrome, which in day of this writing is v. 71.0.3578.98) I have no idea why this is happening or how to fix it.

Could someone help?

Upvotes: 4

Views: 900

Answers (1)

CertainPerformance
CertainPerformance

Reputation: 370789

The problem is that accessing interface.toString first goes through

get: (obj, prop) => Reflect.has(obj, prop)
    ? Reflect.get(obj, prop)
    : Reflect.has(obj[__methods], prop)
        ...

You're expecting interface.toString to fall through the ternary here and get to the _methods, but Reflect.has(obj, 'toString') will evaluate to true because of Object.prototype.toString. Then, invoking that function on the object goes through the proxy's getter operation again, searching for a #toStringTag to call. The getter goes through all its ternaries and finds nothing, so it throws on the line

console.log(`No #${prop} property exists.`)

because prop is a symbol and cannot be concatenated.

One possibility would be to use an object that does not inherit from Object.prototype:

const obj = Object.create(null);
const createInterface = (...props) => new Proxy(
  Object.assign(obj, {
    ...props,
    [__methods]: {
      id: () => createInterface (...props),
      toString: () => `Interface(${ props.toString() })`
    }
  })
  , missingMethod
);

const __methods = Symbol.for("__methods");

const missingMethod = ({
    get: (obj, prop) => Reflect.has(obj, prop)
        ? Reflect.get(obj, prop)
        : Reflect.has(obj[__methods], prop)
            ? Reflect.get(obj[__methods], prop)
            : console.log(`No #${prop} property exists.`)
});

    const obj = Object.create(null);
    const createInterface = (...props) => new Proxy(
      Object.assign(obj, {
        ...props,
        [__methods]: {
          id: () => createInterface (...props),
          toString: () => `Interface(${ props.toString() })`
        }
      })
      , missingMethod
    );

const interface = createInterface(0, 1, 2);
interface.id(); //works
console.log(interface.toString());

Another possibility would be for the getter to do a hasOwnProperty check instead of a Reflect.has check (Reflect.has is basically the same as in, and 'toString' will be in almost any object):

get: (obj, prop) => obj.hasOwnProperty(prop)

const __methods = Symbol.for("__methods");

const missingMethod = ({
    get: (obj, prop) => obj.hasOwnProperty(prop)
        ? Reflect.get(obj, prop)
        : Reflect.has(obj[__methods], prop)
            ? Reflect.get(obj[__methods], prop)
            : console.log(`No #${prop} property exists.`)
});
const createInterface = (...props) => new Proxy({
    ...props,
    [__methods]: {
        id: () => createInterface (...props),
        toString: () => `Interface(${ props.toString() })`,
    }
}, missingMethod);

const interface = createInterface(0, 1, 2);
interface.id(); //works
console.log(interface.toString());

A third possibility would be to make sure the property found by the initial Reflect.has is not from an Object.prototype method:

get: (obj, prop) => Reflect.has(obj, prop) && Reflect.get(obj, prop) !== Object.prototype[prop]

const __methods = Symbol.for("__methods");

const missingMethod = ({
    get: (obj, prop) => Reflect.has(obj, prop) && Reflect.get(obj, prop) !== Object.prototype[prop]
        ? Reflect.get(obj, prop)
        : Reflect.has(obj[__methods], prop)
            ? Reflect.get(obj[__methods], prop)
            : console.log(`No #${prop} property exists.`)
});

const createInterface = (...props) => new Proxy({
    ...props,
    [__methods]: {
        id: () => createInterface (...props),
        toString: () => `Interface(${ props.toString() })`
    }
}, missingMethod);

const interface = createInterface(0, 1, 2);
interface.id(); //works
console.log(interface.toString());

Upvotes: 6

Related Questions