Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to call toPrimitive in Javascript?

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?

like image 485
Yuri Yaryshev Avatar asked Oct 21 '25 11:10

Yuri Yaryshev


1 Answers

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.

like image 95
Matthijs Avatar answered Oct 23 '25 01:10

Matthijs