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,
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