Proposal: Reference Proxy

Hey everyone! I'd like to propose a way to create proxies for primitive. I wrote a small draft here: GitHub - hugoattal/tc39-proposal-primitive-proxy: Primitive proxy proposal

Here's the essence of it:


TC39 proposal: Reference proxy

Synopsis

The proposed reference proxy is aimed at providing a mechanism for creating reference variables with user-defined set and get functions. This allows developers to add custom functionality to primitive types without having to wrap them in objects.

Prerequisite

Maybe:

Motivation

The proposed reference proxy would simplify properties of reactive frameworks such as React or Vue. These frameworks rely heavily on the ability to create reactive properties that can update automatically in response to changes in the underlying data.

There are two solutions:

  • React-like reactive properties use a getter to get the variable, and a function to set it.
  • Vue-like reactive properties wrap the variable into a Proxy, where the value can be set and get using the value property.

A reference proxy would allow setting and getting a reactive variable seamlessly, here's an example:

// React
const [myReactVar, setMyReactVar] = useState("");
setMyReactVar("Hello world");
console.log(myReactVar);

// Vue
const myVueVar = ref("");
myVueVar.value = "Hello world";
console.log(myVueVar.value);

// Primitive proxy
let myReferenceVar = makeRef("");
myReferenceVar = "Hello world";
console.log(myReferenceVar );

Possible API

Here is a possible implementation inspired by the Proxy object, but without the prop argument of the handler get and set.

let myVar = new ReferenceProxy({ storedValue: "" }, {
    set: (target, value) => {
        target.storedValue = value + " world";
    },
    get: (target) => {
        return target.storedValue + " !";
    }
});

myVar = "Hello";
console.log(myVar); // "Hello world !"

Here, storedValue is just for example purpose. The first argument of ReferenceProxy is an object where you can store values that can be accessed with the target argument of set and get.

API details

The ReferenceProxy constructor takes two arguments: target and handler. target is an object to store values, and handler is an object containing the methods to intercept the operations performed on the target. The handler object can have get and set methods, which will be called when the target is accessed or modified.

ReferenceProxy<TTarget, TPrimitive>(
    target: TTarget,
    handler: {
        set: (target: TTarget, value: TPrimitive) => void
        get: (target: TTarget) => TPrimitive
    }
);

Questions

Isn't it confusing not to be able to reassign a variable?

It's the same thing as standard Proxy. Setting a value to myObject.thing does not necessary mean that the thing property of myObject contains the set value, if the object is behind a Proxy.

Why using a let instead of a const?

For consistency, const should not be updatable. So a const Primitive Proxy should disable the set handler.

How to unproxify a reference proxy?

Simply use the valueOf function like this:

let a = new PrimitiveProxy(...);
let b = a.valueOf(); // b is a standard primitive

What happens when passing a Reference Proxy to a function?

When passed to a function, the Reference Proxy is copied just like an object.

let a = new ReferenceProxy({storedValue: ""}, {
    set: (target, value) => {
        target.storedValue = value;
    },
    get: (target) => {
        return "primitive:" + target.storedValue;
    }
});

a = "Hello";
console.log(a); // "primitive:Hello"

((value) => {
    console.log(value); // "primitive:Hello"
    value = "test";
    console.log(value); // "primitive:test"
})(a);

console.log(a); // "primitive:test"

So, when is getter called?

The getter is called when the primitive value of the Reference Proxy is needed.

Here are some examples:

let a = new ReferenceProxy({storedValue: ""}, {
    set: (target, value) => {
        console.log("set");
        target.storedValue = value;
    },
    get: (target) => {
        console.log("get");
        return target.storedValue;
    }
});

a = 5 // set

console.log(a) // get

const b = a; // copy Primitive Proxy
const c = a + 5 // get
const d = 5 + a // get
const e = "Hello" + a // get
const f = (a === 5) // get

What do you guys think of this?

In other words, it seems like you're wanting to be able to hook into the assignment operation and have extra behaviors happen when you assign to certain values. This is technically unrelated to primitives, as the same restriction currently applies with objects - you can't hook into what happens when you reassign a variable holding an object.

let myObj = getProxyObj();
myObj = { x: 2 }
// The proxy doesn't realize that a re-assignment just happened.
1 Like

I think you're confusing the concepts of (primitive) values and variables - or at least you're deliberately mingling them, which I think is a bad idea.
A primitive value is, first and foremost, an immutable value, so it cannot be and does not need to be proxied.
It seems like what you are trying to do is rather to introduce reference values, including proxies around them, and make variables become aliases for the references that they are initialised with. I think it would be cleaner to introduce a completely new keyword for this, like

ref myVar = createReference({get: …, set: …});

So this already exists! Just use an object with a [Symbol.toPrimitive]() method.

@theScottyJam @bergus You guys are right, I most likely don't have the valid term for it...

But basically, yes, I want to hook a function to the assignment operation (as well as the "getting" operation).
Do you guys think that ReferenceProxy would be a better name? Or just Reference?

We say that strings are immutable, and yet you can do this

let x = 'abc';
x = x + 'def';

Did we just mutate that string?

No.

Why?

Because, the thing we updated wasn't the string, rather, it was the variable pointing to the string.

All variables, in JavaScript, are like post cards with addresses - they point to the data, but in-and-of-themselves aren't the data. So, when we do something like x = x + def, what we're doing is taking the "x" post card, following it to its address where we find the data "abc", concatenate that with the string "def", put this new data out there somewhere, grab the address to the new data, then store the new address on our old "x" post card. Never at any time did we "edit" the original "abc" string.

Objects work the same way, except they're mutable, which is why if you have multiple post cards pointing to the same object, then you mutate a property, all post cards will see that mutation.

This proposal feels like it's trying to work with the misconception that "variables are the data" rather than "variables point to the data". If variables are the data, then it makes sense that you could have a proxy that hooks into the assignment operation. But, variables are just pointing to the data. This means, the operation of "changing the address of a post card" really has nothing to do with what that post card is pointing to. These are two completely different things. And I would find it very odd if the data my post card pointed to, had some sort of influence on what I was doing with my post card to that data. I, personally, would prefer to keep the "post cards" and "data" as two separate things - the data shouldn't influence the post cards.

Don't know if any of that makes sense.

Thanks a lot for the explanation, I think I get what you mean. (You're explaining the concept of addresses and pointers, is that right?)

But we can already hook some logic to the variable valueOf() method like this:

function MyNumber(n) {
  this.number = n;
}

MyNumber.prototype.valueOf = function() {
  return this.number++;
};

let a = new MyNumber(5);
console.log(a + 0); // 5
console.log(a + 0); // 6
console.log(a + 0); // 7

Here, a is an object, but when used in operations, we can trigger some kind of logic.
So there's already some "magic" happening when using a variable. Even doing something like "test" + 123 works and return "test123".

I just want to have this kind of "magic" for assignation as well. Like, if the variable is a "VarProxy" (or whatever this would be called), then the = symbol would trigger some kind of function like VarProxy.prototype.setValue for example.

Would that be compatible with what you said?

Yeah, I guess that's a fair counter argument.

Have you ever taken a peek at Svelte? They found their own way to solve this problem - granted they basically invented a language that's almost the same as JavaScript, and has the same syntax as JavaScript, but also has a couple of super powers.

In svelte, if you prefix a declaration with "$:", it means the variable should auto update whenever a value that was used to initialize that variable got updated. I believe the places you use this special variable will also auto update (I'm just going off of memory, I've never actually used it). I believe this is basically solving the use-case you're proposing. And, maybe it's worth seeing if we could bring some of those Svelte mechanics into the native language so they don't have to extend JavaScript to do what they want.

1 Like

A prerequisite to this might be something like @rbuckton 's Ref proposal GitHub - rbuckton/proposal-refs: Ref declarations and expressions for ECMAScript. As that would add the core construct of what a reference is, and something that could be intercepted.

4 Likes

@theScottyJam That's very interesting! It seems like it's a sugar syntax for a computed property.
And the computed property of Vue (the framework I mostly use) is very close to what I want to achieve.

Here is an example from this documentation page:

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // getter
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter
  set(newValue) {
    // Note: we are using destructuring assignment syntax here.
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

But of course, it wraps everything inside an object proxy, so you must assign and read it with the value property like this:

fullName.value = "John Doe";
console.log(fullName.value);

@aclaymore Thank you very much, that would make a lot more sense!
I'll rename this "ReferenceProxy" then !

I would expect this to be a feature of a let decorator proposal. Ref declarations are a much more powerful feature that would face quite a bit more opposition than simply being able to add behavior to get/set of a binding.

I would like to use a keyword ref or exp that may behave as below. Here I use ref:

let obj = { x: 1};
ref x = obj.x;
x=2; // obj.x = 2
console.log(x); // 2

delete obj.x;
x=2; //false;
console.log(x); // undefined
obj.x = 3;
console.log(x); // 3

deref x; // x no longer refer to obj.x

let obj = { x: 1, o: { y: 2}};
ref y = obj.o.y; // y likes an expression rather than a reference
y = 3; // obj.o.y = 3; 

ref hasOwn = obj.hasOwnProperty;
hasOwn("x"); // true

ref hasOwn = obj.__proto__.hasOwnProperty;
// still works and all own properties of obj are bypassed
hasOwn("x"); // true