javascriptobjectpropertiesobservablepropertydescriptor

How to observe property value changes of a third party object?


I would like to observe whenever a property of a third party object is changed. I'm taking the approach of assigning a custom setter but my console.log below is never invoked. Why is that? Is there a better approach?

const foo = { a: 1, b: 2 };

Object.assign(foo, {
  set user(user) {
    foo.user = user;
    console.log(">>>>>> user was changed", user);
  },
});

// Desired behaviour
foo.user = "asdf"; // >>>>>> user was changed asdf
delete foo.user; // >>>>>> user was changed undefined
foo.user = "asdf1" // >>>>>> user was changed asdf1

Please note, I need to mutate foo I cannot wrap a proxy around foo and return that because it is a third party library which mutates .user internally


Solution

  • I've found a way, pretty hacky as it is

    const foo = { a: 1, b: 2 };
    
    let underlyingValue = foo.user
    
    Object.defineProperty(foo, "user", {
      get() {
        return underlyingValue
      },
      set(user) {
        underlyingValue = user;
        console.log(">>>>>> user was changed", user);
      },
      enumerable: true
    });
    
    foo.user = "asdf";
    console.log(foo)

    I've made this into a generic function below 👇

    /** Intercepts writes to any property of an object */
    function observeProperty(obj, property, onChanged) {
      const oldDescriptor = Object.getOwnPropertyDescriptor(obj, property);
      let val = obj[property];
      Object.defineProperty(obj, property, {
        get() {
          return val;
        },
        set(newVal) {
          val = newVal;
          onChanged(newVal);
        },
        enumerable: oldDescriptor?.enumerable,
        configurable: oldDescriptor?.configurable,
      });
    }
    
    // example usage 👇
    const foo = { a: 1 };
    observeProperty(foo, "a", (a) => {
      console.log("a was changed to", a);
    });
    foo.a = 2; // a was changed to  2

    Also available in typescript

    🚨 Edit: This will break if the property is deleted eg delete foo.user. The observer will be removed and the callback will stop firing. You will need to re-attach it.