Reputation: 2279
I'm trying to implement a decorator that overrides a property (1) and defines a hidden property (2). Assume the following example:
function f() {
return (target: any, key: string) => {
let pKey = '_' + key;
// 1. Define hidden property
Object.defineProperty(target, pKey, {
value: 0,
enumerable: false,
configurable: true,
writable: true
});
// 2. Override property get/set
return Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: () => target[pKey],
set: (val) => {
target[pKey] = target[pKey] + 1;
}
});
};
}
class A {
@f()
propA = null;
propB = null;
}
let a = new A();
console.log(Object.keys(a), a.propA, a._propA, a);
Which outputs:
[ 'propB' ] 1 1 A { propB: null }
However, I would rather expect:
[ 'propA', 'propB' ] 1 1 A { propA: 1, propB: null }
since enumerable
is true
for propA
.
Now, if I replace get
and set
with
get: function () {
return this[pKey]
},
set: function (val) {
this[pKey] = this[pKey] + 1;
}
the output is now:
[ '_propA', 'propB' ] 1 1 A { _propA: 1, propB: null }
Though enumerable
is explicitly set to false
for _propA
in f
.
So, as weird as these behaviours can be, I'd like to understand what is going on here, and how I would implement what I'm trying to get ?
Upvotes: 2
Views: 4062
Reputation: 2279
Alright, so it took me a while, but I found a workaround. The problem seems to be that Object.defineProperty
does not work properly at decoration-time. If you do it at runtime, things go as expected. So, how do you define the property inside the decorator, but at runtime ?
Here is the trick: since overriding the property inside the decorator works at decoration time (only the enumerable behavior seems to be broken), you can define the property but use an initialisation function in place of getter
and setter
. That function will be run the first time the property is assigned (set
) or accessed (get
). When that happens, the word this
references the runtime instance of your object, which means you can properly initialise what you intended to do at decoration-time.
Here is the solution:
function f() {
return (target: any, key: string) => {
let pKey = `_${key}`;
let init = function (isGet: boolean) {
return function (newVal?) {
/*
* This is called at runtime, so "this" is the instance.
*/
// Define hidden property
Object.defineProperty(this, pKey, {value: 0, enumerable: false, configurable: true, writable: true});
// Define public property
Object.defineProperty(this, key, {
get: () => {
return this[pKey];
},
set: (val) => {
this[pKey] = this[pKey] + 1;
},
enumerable: true,
configurable: true
});
// Perform original action
if (isGet) {
return this[key]; // get
} else {
this[key] = newVal; // set
}
};
};
// Override property to let init occur on first get/set
return Object.defineProperty(target, key, {
get: init(true),
set: init(false),
enumerable: true,
configurable: true
});
};
}
Which outputs:
[ 'propA', 'propB' ] 1 1 A { propA: [Getter/Setter], propB: null }
This solution supports default values, because they are assigned after the correct get/set have been initialised.
It also supports enumerable
properly: set enumerable
to true
for property pKey
and the output will be:
[ '_propA', 'propA', 'propB' ] 1 1 A { _propA: 1, propA: [Getter/Setter], propB: null }
It's not pretty, I know that, but it works and does not reduce performance as far as I know.
Upvotes: 4
Reputation: 13
I inspected your code and see that it will define the property twice. I modified your code.
class A {
@dec
public value: number = 5
}
function dec(target, key) {
function f(isGet: boolean) {
return function (newValue?: number) {
if (!Object.getOwnPropertyDescriptor(this, key)) {
let value: number;
const getter = function () {
return value
}
const setter = function (val) {
value = 2 * val
}
Object.defineProperty(this, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
})
}
if (isGet) {
return this[key]
} else {
this[key] = newValue
}
}
}
Object.defineProperty(target, key, {
get: f(true),
set: f(false),
enumerable: false,
configurable: false
})
}
const a = new A()
console.log(Object.keys(a))
And we will get in console
["value"]
Upvotes: 0