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?
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());