Is there a way to call toPrimitive on a value in Javascript?
Why?
I'm using Proxy in my project.
function makeProxy(unknown_value) {
let proxy_target = ()=>{};
proxy_target.inner = unknown_value;
proxy_target.my_data = 123456;
return new Proxy(proxy_target, some_proxy_handler);
}
let proxied_value = makeProxy(<? something unknown ?>);
This proxy redirects all the calls to "inner". And eventually I was stroke by something like this:
console.log(1+proxied_value);
Here Javascript calls
(1)
proxied_value.get(proxy_target, Symbol.toPrimitive)
Because Javascript wants to try to convert proxied_value to some value to perform "+" for it. And this operation IS possible for proxy_target.inner
So I want to redirect this call (1) to:
(something like)
Object.toPrimitive( proxy_target.inner );
But I found no way to call toPrimitive directly. Is there a way?
Unfortunately, based on searching the spec for all references to ToPrimitive, there doesn't seem to be any option other than tediously reimplementing the algorithm in javascript.
A (mostly untested) example implementation might be:
// a primitive is anything that's not an object, but typeof has two quirks
// to be cautious of:
// - for a callable object it returns 'function' rather than 'object'
// - typeof null is 'object' even though null is a primitive
const isPrimitive = ( value ) =>
typeof value === 'object' ? value === null : typeof value !== 'function';
const toPrimitive = ( value, hint='default' ) => {
// if value is already a primitive, we're done
if( isPrimitive( value ) )
return value;
// if value[ Symbol.toPrimitive ] exists (is not undefined or null)
let method = value[ Symbol.toPrimitive ];
if( method != null ) { // using != instead of !== is intentional
// then call value[ Symbol.toPrimitive ]( hint )
let result = Reflect.apply( method, value, [ hint ] );
if( isPrimitive( result ) )
return result;
// if it doesn't return a primitive, do not fall back to legacy
// methods but just throw an error
} else {
// if value[ Symbol.toPrimitive ] doesn't exist, try the legacy
// valueOf() and toString() methods, in that order unless hint
// is 'string' in which case toString() is tried first.
//
// for these we need to be more garbage-tolerant: if the first
// legacy method is a non-function or returns a non-primitive,
// just ignore it and move on to the second one.
method = value[ hint === 'string' ? 'toString' : 'valueOf' ];
if( typeof method === 'function' ) {
let result = Reflect.apply( method, value, [] );
if( isPrimitive( result ) )
return result;
}
method = value[ hint === 'string' ? 'valueOf' : 'toString' ];
if( typeof method === 'function' ) {
let result = Reflect.apply( method, value, [] );
if( isPrimitive( result ) )
return result;
}
}
throw new TypeError("Cannot convert object to primitive value");
};
If you don't need a fully accurate implementation of ToPrimitive but merely something functionally equivalent when used in your application then you can simplify the implementation to:
const toPrimitive = ( value, hint='default' ) => {
if( isPrimitive( value ) )
return value;
let method = value[ Symbol.toPrimitive ];
if( method != null ) // using != instead of !== is intentional
return Reflect.apply( method, value, [ hint ] );
method = value[ hint === 'string' ? 'toString' : 'valueOf' ];
if( typeof method === 'function' ) {
let result = Reflect.apply( method, value, [] );
if( isPrimitive( result ) )
return result;
}
method = value[ hint === 'string' ? 'valueOf' : 'toString' ];
if( typeof method === 'function' )
return Reflect.apply( method, value, [] );
return value;
};
and leave throwing exceptions to the caller (the Javascript runtime).
Note that method != null (or method != undefined) is simply a more concise (and slightly more efficient) way to write method !== undefined && method !== null.
Beware to avoid writing something like:
if( value[ Symbol.toPrimitive ] != null )
return value[ Symbol.toPrimitive ]( hint );
since this performs the value[ Symbol.toPrimitive ] property lookup twice, which is an observable difference from the specification (and doing the same property lookup twice can yield different values).
Also don't be tempted to write method.call( value ) instead of Reflect.apply( method, value, [] ) since that will not have the same result if the method (rudely) overrides its call method. Using Function.prototype.call.call( method, value ) is fine, but I wouldn't consider that to be an improvement over using Reflect.apply.
You might be able to optimize the case where hint === 'string' to:
const toPropertyKey = ( value ) => {
return Reflect.ownKeys( { [value]: 0 } )[0];
};
since I think that should be compatible with all uses of ToPrimitive with that hint that are currently in the spec, but there is a possibility it could break in the future (e.g. if a future spec introduces bytestrings similar to python3's bytes type). I also don't know whether this is actually faster, that would require testing.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With