Search code examples
typescriptdecoratortypescript-decorator

TypeScript: get wrong object when use Property Decorator


I have a class Car with some info of a car include plate, now I want validate the property plate, so I use a property decoratorin this way:

class Car{
    @validate 
    public plate: string;

    public model: string;
    // extra info

    constructor(plate: string, model: string){ 
        this.plate= plate; 
        this.model = model;
    }

    toString(): string{
       return `Car: ${this.plate} - ${this.model}`;
    }
}

Then I have the following property decorator function:

function validate(target: any, propertyKey: string){
    let value = target[propertyKey];
    Object.defineProperty(target, propertyKey, {
       get: () => value,
       set: (newValue) => {            
           const pattern = /^[A-Z]{2}\s?[0-9]{3}\s?[A-Z]{2}$/;
           if(pattern.test(newValue)){
               value = newValue;
           } else {
               console.error('Non valid plate: ', newValue);
               //value = undefined;
           }
       }
   })
}

Now, if I test my code in this way:

const car1 = new Car('IT123UE', 'Car1Model');
console.log('1 - ', car1.toString());

const car2 = new Car('IT000000UE', 'Car2Model');
console.log('2 - ', car2.toString());

I get:

1 - Car: IT123UE - Car1Model
Non valid plate:  IT000000UE
2 - Car: IT123UE - Car2Model  <-- why print car1.plate if car2.plate is not valid and this is car2 object?

I have solved using value = undefined; in the else inside my validation function but I don't figure out why in my car2.plate I get car1.plate value. Another test, if I change the order:

const car2 = new Car('IT000000UE', 'Car2Model');
console.log('2 - ', car2.toString());

const car1 = new Car('IT123UE', 'Car1Model');
console.log('1 - ', car1.toString());

I get:

Non valid plate:  IT000000UE
2 -  Car: undefined - Car2Model <- now is undefinied
1 -  Car: IT123UE - Car1Model

what I expected, but why didn't it work before?

I'm using TS 3.4.5 with VS Code, and in tsconfig I have

"target": "es6",
"experimentalDecorators": true,     

Solution

  • why print car1.plate if car2.plate is not valid and this is car2 object?

    Short answer: The reason is that both class instances are accessing the same class prototype property, and that prototype property is shared state between the class instances.

    Longer answer: Your validate function is a property decorator on an instance property. The TypeScript documentation says that instance property decorators receive the class prototype as the first argument. As a result, inside your validator, you are setting the plate property of the class prototype not of the specific class instance. Since class instances share the class prototype, the second class instance accesses the property value that the first instance already set.

    Here is a demo that illustrates two approaches; the second approach will work for you. The first is what you originally did with shared prototype state. The second (validateToo) does not use shared state; instead of operating on target, the get/set operate on this, and instead of being arrow functions, the get/set become functions, so that they take the correct this object.

    class Car {
        @validate
        public plate: string;
    
        @validateToo
        public plateToo: string;
    
        constructor(plate: string) {
            this.plate = plate;
            this.plateToo = plate;
        }
    }
    
    // this second approach will work for you
    function validateToo(target: any, propertyKey: string) {
        const fieldKey = `_${propertyKey}`;
        Object.defineProperty(target, propertyKey, {
            get() {
                return this[fieldKey];
            },
            set(newValue) {
                // you can put your validation logic here
                this[fieldKey] = newValue;
            }
        })
    }
    
    function validate(target: any, propertyKey: string) {
        let value = target[propertyKey];
        Object.defineProperty(target, propertyKey, {
            get: () => value,
            set: (newValue) => value = newValue
        })
    }
    

    Demo

    const car1 = new Car('one');
    const car2 = new Car('two');
    const car3 = new Car('three');
    
    console.log(car1.plate, car1.plateToo); // three, one
    console.log(car2.plate, car2.plateToo); // three, two
    console.log(car3.plate, car3.plateToo); // three, three