Reputation:
I've just started to learn in more depth Typescript and ES6 capabilities and I have some misunderstandings regarding how you should create and how it actually works a custom Property Decorator.
This are the sources that I'm following Source1 Source2
And this is my custom deocrator.
export const Required = (target: Object, key: string) => {
let value: any = target[key];
const getter = () => {
if (value !== undefined) return value;
throw new RequiredPropertyError(MetadataModule.GetClassName(target), key, ErrorOptions.RequiredProperty)
}
const setter = (val) => value = val;
if (delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
}
It is applied like so
export classMyClass{
@Required
public Items: number[];
}
What I don't understand is why it works differently then what would you expect. Well it works but I don't know if it works accordingly to the let's call it "decorator philosophy",
Let me explain what I don't understand
Starting with the first line of code.
let value: any = target[key];
I would expect that the value
would be initialized with Items
value, but instead is undefined
, why? how? i really don't understand.
I've followed both sources and that first thing that I find it confusing is the fact that one used target[key]
to initialize the value
while the other used this[key]
, shouldn't this
refer to the Required actually.
What I also find confusing is this part
if (delete this[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
}
Firstly, why I need to delete this[key] ? from my understanding this should refer to the current object context in my this case Required
, and when debugged it is as so.
Secondly, that Object.defineProperty will create a property with the name of the key on the target class in my case MyClass
, but isn't this property already there?
Moving forward and setting the value
using the setter, how does the setter parameter val
know what data it should hold ? I mean where is it coming ?
Thanks all.
Upvotes: 1
Views: 1633
Reputation: 12814
There's a lot of questions in there, but I'll try to answer them all :)
I would expect that the
value
would be initialized withItems
value, but instead isundefined
, why?
When your MyClass
class is instantiated and your decorator code is run, the value of all properties of the object are undefined
(even if you use a TypeScript property initializer, which you aren't).
Shouldn't
this
refer to theRequired
?
Yes, I think it does in your example. But in your Source1 link, the decorator is defined using a function expression (function logProperty()
); in your example, you have switched this to an arrow function expression (const Required = ( ... ) =>
). It's likely this switch will change what this
refers to. To be safe, switch this to target[key]
.
Why I need to delete this[key]?
This block of code is deleting the original Items
property and replacing it with a property of the same name that allows you to spy on the getting and setting of the variable. The delete this[key]
line is guarding against the case where the property isn't configurable. (The delete
operator returns false
if the property in question is non-configurable:).
Object.defineProperty will create a property with the name of the key on the target class in my case
MyClass
, but isn't this property already there?
Yes, as mentioned above - this code is replacing the property with a new property. The new property is set up to allow you observe the getting and setting of this variable.
Moving forward and setting the
value
using the setter, how does the setter parameterval
know what data it should hold ? I mean where is it coming ?
When your new Items
property is set, the setter
function is called with the soon-to-be new value of the property as a parameter (this setter
function was previously set up as a setter function using Object.defineProperty
). The implementation of this setter
function sets value
, which is a reference to target[key]
. As a result, the value of Items
gets updated.
To better understand how all this is working, try adding in some logging statements and then play around with getting/setting the Items
property:
export const Required = (target: Object, key: string) => {
let value: any = target[key];
console.log('Initialize the Required decorator; value:', value);
const getter = () => {
console.log('Inside the getter; value is:', value);
if (value !== undefined) return value;
throw new RequiredPropertyError(MetadataModule.GetClassName(target), key, ErrorOptions.RequiredProperty);
};
const setter = val => {
console.log('Inside the setter; val is: ', val, 'value is:', value);
return (value = val);
};
if (delete this[key]) {
console.log('Replacing the "' + key + '" property with a new, configured version');
Object.defineProperty(target, key, {
get: getter,
set: setter,
enumerable: true,
configurable: true
});
}
};
export class MyClass {
@Required
public Items: number[];
}
// instantiated your class
var mc = new MyClass();
// set the Items property
mc.Items = [4, 5, 6];
// get the value with no Errors
mc.Items;
// set the Items property to undefined
mc.Items = undefined;
// get the value - an Error is thrown
mc.Items;
Upvotes: 1