The purpose here is to create an optional property of object-like identifiers (object, function) that permits short-hand/circuit boolean tests to be controlled when one or more operands is object-like.
My specific use-case is basically proxied booleans:
// Simplified - doesn't work without some binding, but that's not the issue
var f = new Proxy(new Boolean(false),{...}) ;
// This is problem I'm trying to address
var foo = f && "foo"; // = "foo", unexpected
var bar = f ? "tbar" : "fbar"; // = "tbar", unexpected
var bax;
if (f) bax = 1 else bax = 100; // =1, unexpected
...because t
is an object, and objects are always true
unless it is null
.
The proposal is to create an optional symbol with the signature:
[Symbol.toBoolean]() { return <primitive boolean expression> }
The implementation would be:
- Check if the operand supports properties (eg object or function)
- If it does, check for the property [Symbol.toBoolean]
- If it does, check the property value is a function
- If it is, invoke the function with
this
set to be the object being coerced to a boolean
- Substitute the value being coerced with the return value (optionally: check the return type and throw TypeError if not a primitive)
- ...continue with the EXISTING JS type coercion to truthy/falsy values
This algorithm would be implemented within the existing engines call hierarchy, and there invoked in all the cases above:
- boolean operators
!
, &&
||
etc,
- boolean assignments
&&=
etc
- ternary operator determiner
expr ?
if
, while
, for
conditions
Does this break the web? No. No existing implementations can use the well-defined Symbol toBoolean.
It permits any object to appear in a statement such as:
if (!customerAddress) throw new Error("The address is in the incorrect format")
...where customerAddress
is an object, including a Proxy for some other object or value.
Note: although the use-case seems heavily Proxy-based, the solution is not. Engines currently never assess any object-like value as "falsy".
All feedback welcome. I can provide the actual code-base in which this would prove advantageous if anyone has the inclination to understand the details
Reference:
A risk here is that it makes checking if something is truthy more expensive, an object can no longer early return and instead the runtime also has to look up to see if the symbol protocol is present.
It may also make code harder to reason about compared to the simpler "all objects are falsey" rule.
2 Likes
I completely accept the performance argument. I'm not wedded to the implementation, and a different, faster mechanism could be used instead, although my understanding is the property lookup is pretty quick. Obviously, it would only be necessary where objects appear in boolean expressions, although I accept this is quite a common case when a function returns an object.
Perhaps it would help if I were more explicit about the use-case I have. My library can create objects that are both async iterators and "values" within an object. In typescript they are typed as "V & AsyncIterableIterator".
This works very will with new String("abc")
and new Number(123)
in that I can use "thing.n += 23" where "thing.n" is of the above-mentioned type. I can also say for await (const n of thing.n) {...}
which also functions as expected (it yields every time thing.n is assigned). The only common type it doesn't work with is new Boolean(false)
. I can consume changes in the value, but to use it as expected, I have to evaluate thing.b.valueOf()
, since Boolean#valueOf() returns the unboxed, primitive value. (Of course, if (thing.n)
fails to "fail" when the value is 0. I would have to implement the toBoolean property on the proxied Number() to do that).
The performance issue you raise is already present in the type coercion algorithm for strings and numbers, with an apparently acceptable cost. Reasoning about type coercion is already not simple, however this change would make booleans more consistent in their handling when compared to other boxed primitives, although like lots of JS it could be abused.
The purpose here is to allow the creation of a type that predictably can fail an if (identifier)
test, to allow for the more natural use than if (identifier.valueOf())...
, as that assumes the developer has knowledge of the type (and therefore the underlying implementation) of whatever library returned identifier
, and knows to add the valueOf().
All the above having been said, I understand change is hard, and it may be that this use-case is just too specific for some tastes.
Thanks! I appreciate you taking the time to reply.
This adds more overhead than the existing logic for strings and numbers.
For strings and some numbers (see below, it will need to load the value from the heap, but it doesn't then need to do any dynamic lookup or potential invoke more JS. What it needs to check is exactly defined. This code is safer to optimise. JIT compilers can't make as many assumptions when it might re-enter JS.
For numbers, most engines uses pointer tagging or NaN boxing. Which means that for certain sets of numbers it can check the value directly, without loading anything from the heap.
1 Like