How to avoid redefining error when overriding property via typescript decorators

387 views Asked by At

I use object-oriented style when defining data for my interface. Since Vue 3 provided Composition API, simply assigning instances of my classes to properties of data object inside component is some sort of deprecated way only for backward compatibility.

I'm trying to write decorators for wrap properties of class instances in ref and reactive. It looks like this (naming only for example purposes):

@UseVueReactivity
class X {
  @Ref
  a = 10

  @Reactive
  b = { foo: 'bar' }

  @ShallowReactive
  c = { a: { deepA: 'deepAValue' } }

  // ... and so on
}

The implementation is simple: decorators like @Ref, @Reactive just add keys to array, that stored in class prototype by specific symbol key. Values of that keys should be wrapped by pair of accessors in @UseVueReactivity, just like in example below (some type annotations omitted for better readability).

const refKeysSymbol = Symbol('Ref keys symbol')
const reactiveKeysSymbol = Symbol('Reactive keys symbol')
// ... and so on

const Ref = (target, key) => {
  // Skipping part, when we assure that array by symbol key exists
  target[refKeysSymbol].push(key)
}

const UseVueReactivity = <T extends Function>(Original: T): T => {
  return class extends Original {
    constructor (...args) {
      super(...args)
    }

    const refPropKeys = Extendable.prototype[refKeysSymbol] as string[] ?? []
    
    for (const key of refPropKeys) {
      const refPropKey = `Ref.${key}` // Key for result of ref()

      Reflect.defineProperty(this, refPropKey, {
        value: ref(this[key]) // Trying to set initial value, the first part of question
      })

      Reflect.defineProperty(this, key, {
        get () {
          return this[refPropKey].value
        },
        set (v) {
          this[refPropKey].value = v
        },
        configurable: true
      })
    }
  }
}

Now, example for the question, where error from title occures. Suppose we have class A and B, that extends A

@UseVueReactivity
class A {
  @Ref
  foo = 10
}

@UseVueReactivity
class B extends A {
  @Ref
  bar = 20
}

const a = new A()
const b = new B()

If we log a in Chrome Devtools we will see something like this:

{
  Ref.foo: RefImpl { ... }
  get foo: f get()
  set foo: f set(v)
  ...
}

Property value will remain: a.foo = 10. But if we log b and will try to change its bar value, we will receive undefined as start value for b.bar and consequently NaN as a result of, for example, b.bar++.

If we remove configurable: true, in @UseVueReactivity we will get a

Uncaught TypeError: Cannot redefine property: bar
  at B.<instance_members_initializer>
  ...

I assume that even if I use Reflect.defineProperty to define accessors for b.bar, the code @Ref bar = 20 or even @Ref bar in B class definition will redefine my accessors, that is why error is throws if I leave configurable flag with false. But I don't want class to overwrite a overrided property.

My questions are: What am I doing wrong in redefinition properties when decorating already decorated class and what am I supposed to do, to fix this?

May be I miss some details about prototype extending in JS, but I can't take it all together. I appreciate if you point on specs or MDN, where that sort of details mentioned.

Thank you.

0

There are 0 answers