简体   繁体   中英

Can typescript property decorators modify instance members instead of the entire class?

I want to write a sanitizer decorator which I can put on all user-input string fields. This simply replaces the standard .set(newValue) with .set( sanitize(newValue) ) . However I have found the below code only works for one instance. A second instance of the same class ends up sharing the currentValue. After further reading this is actually expected, but I can't work out how to make it per-instance.

import "reflect-metadata";

export const Sanitize = () => {
    return (target: any, propertyKey: string | symbol) => {
        let currentValue: any = sanitiseString(options, `${target[propertyKey] || ''}`);

        Reflect.deleteProperty(target, propertyKey);

        Reflect.defineProperty(target, propertyKey, {
            get: () => currentValue,
            set: (newValue: string) => {
                currentValue = sanitiseString(newValue);
            },
        });
    }
}

Edit 1: Minimum reproducible example:

import "reflect-metadata";

const sanitiseString = (valToSanitise: string) => {
  // do some stuff, return clean value
  return valToSanitise;
}

const Sanitize = () => {
  return (target: any, propertyKey: string | symbol) => {
    let currentValue: any = sanitiseString(`${target[propertyKey] || ''}`);

    Reflect.deleteProperty(target, propertyKey);

    Reflect.defineProperty(target, propertyKey, {
      get: () => currentValue,
      set: (newValue: string) => {
        currentValue = sanitiseString(newValue);
      },
    });
  }
}

class UserInput {
  constructor(propOne: string, propTwo: string, propThree: number) {
    this.propOne = propOne;
    this.propTwo = propTwo;
    this.propThree = propThree;
  }

  @Sanitize() propOne: string
  @Sanitize() propTwo: string
  propThree: number
}

const inputOne = new UserInput('input 1, prop 1', 'input 1, prop 2', 1)
const inputTwo = new UserInput('input 2, prop 1', 'input 2, prop 2', 2)

console.log(inputOne)
console.log(inputTwo)

// expected output: 
// [LOG]: UserInput: {
//    "propOne": "input 1, prop 1",
//    "propTwo": "input 1, prop 2",
//    "propThree": 1
// } 
// [LOG]: UserInput: {
//    "propOne": "input 2, prop 1",
//    "propTwo": "input 2, prop 2",
//    "propThree": 2
// } 
//  
// actual output: 
//
// [LOG]: UserInput: {
//    "propThree": 1
// } 
// [LOG]: UserInput: {
//    "propThree": 2
// } 
// When you remove @Sanitize() the fields appear in console.log. When you add @Sanitize() the fields disappear.
// Further, forcing console.log(inputOne.propOne) returns [LOG]: "input 2, prop 1" 
// indicating that the property is being written for the class proto and not per instance

console.log(inputOne.propOne)

The main issue here is that Sanitize() gets called once per decorated class property declaration, and so for any given class property there will be only one currentValue . Meaning that two instances of the class will share the same currentValue . If you want to store one value per decorated class property per class instance , then you need access to class instances, and you will have to store the value either in those instances (via a property key that isn't going to interfere with any other properties), or in some map whose key is those instances. In the following I will show how to store the value in the class instances, and to avoid worrying about property name collision I will use the symbol output of the Symbol function , which is guaranteed to be unique.

Also note that Sanitize() is passed the class prototype as the target argument, so any manipulation you perform on target will affect the prototype and not any instance of the class. When you write target[propertyKey] , you are looking up the property in the class prototype, and string -valued properties will almost certainly not be set in the prototype. So this is probably not necessary or useful, and we should get rid of it.

So if you only have direct access to the class prototype, how do you do anything with class instances? Well, to do this, you should use the this context of the get method and set method of the accessor property descriptor you pass to defineProperty() . And that means get and set need to be methods or at least function expressions , and not arrow function expressions which have no distinct this context.


Okay, enough explanation, here's the code:

const Sanitize = () => {
  return (target: any, propertyKey: string | symbol) => {
    const privatePropKey = Symbol();
    Reflect.defineProperty(target, propertyKey, {
      get(this: any) {
        return this[privatePropKey]
      },
      set(this: any, newValue: string) {
        this[privatePropKey] = sanitiseString(newValue);
      },
    });
  }
}

And let's make sure it works as you expect. Let's have sanitiseString actually do something:

const sanitiseString = (valToSanitise: string) => {
  return valToSanitise+"!"; 
}

And now for our class:

class UserInput {
  constructor(propOne: string, propTwo: string, propThree: number) {
    this.propOne = propOne;
    this.propTwo = propTwo;
    this.propThree = propThree;
  }
  @Sanitize() propOne: string
  @Sanitize() propTwo: string
  propThree: number
}

And finally let's see if it works:

const inputOne = new UserInput('input 1, prop 1', 'input 1, prop 2', 1)
const inputTwo = new UserInput('input 2, prop 1', 'input 2, prop 2', 2)

console.log(inputOne.propOne, inputOne.propTwo, inputOne.propThree)
console.log(inputTwo.propOne, inputTwo.propTwo, inputTwo.propThree);
// GOOD OUTPUT
// [LOG]: "input 1, prop 1!",  "input 1, prop 2!",  1 
// [LOG]: "input 2, prop 1!",  "input 2, prop 2!",  2 

Looks good. Each instance of UserInput has its own sanitized propOne and propTwo properties.

Playground link to code

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM