user9124444
user9124444

Reputation:

Typescript / Javascript custom Property decorators

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

Answers (1)

Nathan Friend
Nathan Friend

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 with Items value, but instead is undefined, 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 the Required?

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 parameter val 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

Related Questions