Dalej poznajemy dekoratory w TypeScript, choć to trudny temat. Tym razem będziemy już bez problemu stackować dekoratory do walidacji properties.

Ok, rzućmy okiem na ten przykład z poprzedniej lekcji:

@WithRender('ul-app')
class PersonItem {
    @required
    public name: string;
    // @aboveZero
    @max(100)
    @min(1)
    public age: number;
   
    constructor(name: string, age: number){
        this.name = name;
        this.age = age;
    }
    @enumerable(true)
    @deprecated
    sayHello() {
      console.log(`Hello, my name is ${this.name}!`);
    }
}

Dekoratory metody sayHello stackują się bez problemu. Oto te dekoratory:

function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
      descriptor.enumerable = value;
    };
  }

function deprecated(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalDef = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.warn(`Warning: ${key}() is deprecated. Use other methods instead.`);
    return originalDef.apply(this, args);
  };
  return descriptor;
}

Swoją drogą warto zwrócić uwagę, że return descriptor jest opcjonalne. W TS zresztą wiele rzeczy jest tak na słowo honoru, dzisiaj na przykład zrobiłem type guard na to, czy coś nie jest undefined a i tak nic mi to nie dało. TS to takie coś pomiędzy statycznym i dynamicznym typowaniem, pomiędzy JSem i czymś takim jak C# (choć to zupełnie inna bajka). Nieważne.

Ok, to teraz wbijmy sobie do głowy, że te nasze property dekoratory się nie stackują. Idą od dołu do góry i ten wyżej nadpisuje ten niżej. I jest na to rozwiązanie, ale wpierw musiałem w tsconfig takie coś ustawić, aby móc pobierać deskryptory:

{
    "compilerOptions": {
        // ...
        "target": "es5",
        "lib": ["esnext", "dom"]
        // ...
    }
}

Ok, nieważne co to robi, ważne że działa. Generalnie chodzi o dostępność tych metod klasy Object. Ok, zmieńmy lekko min:

function min(minNum: number) {
  return function(target: any, key: string) {
    let currentValue = target[key];

    console.log(Object.getOwnPropertyDescriptors(target));
    console.log(Object.getOwnPropertyDescriptor(target, key));
    console.log(Object.getOwnPropertyDescriptor(target, key)?.set);

    Object.defineProperty(target, key, {
      set: (newValue: number) => {
        
        if (newValue < minNum) {
          throw new Error(`${key} is too small!`);
        }
        currentValue = newValue;
        
      },
      get: () => currentValue,
      configurable: true
    });
  }
}

Jak widać tam może być undefined. I w min będzie undefined (jeszcze). Ale min zrobi define property i już dalej tam coś będzie. Możemy te console logi do max wrzucić, ja nie będę, bo już ten dekorator napisałem:

function max(maxNum: number) {
  return function(target: any, key: string) {

    let possibleSetter = Object.getOwnPropertyDescriptor(target, key)?.set

    if(possibleSetter === undefined){
      let currentValue = target[key];
      Object.defineProperty(target, key, {
        set: (newValue: number) => {
          
          if (newValue >= maxNum) {
            throw new Error(`${key} is too big!`);
          }
          currentValue = newValue;
          
        },
        get: () => currentValue,
        configurable: true
      });
    } else if(possibleSetter) {
      
      Object.defineProperty(target, key, {
        set: (newValue: number) => {
          
          if (newValue >= maxNum) {
            throw new Error(`${key} is too big!`);
          }
          possibleSetter!(newValue);
          
        },
        configurable: true
      });
    }
  }
}

Myślałem, że szału dostanę, tym bardziej, że w TS nie ma tak naprawdę, że coś jest tak albo inaczej. Tylko jeden wielki mega bełkot. Bez tego wykrzyknika się nie dało, inaczej mi wmawiał, że possibleSetter jest undefined, choć próbowałem na milion sposobów.

Ok, teraz można i min zmienić:

function min(minNum: number) {
  return function(target: any, key: string) {

    let possibleSetter = Object.getOwnPropertyDescriptor(target, key)?.set

    if(possibleSetter === undefined){
      let currentValue = target[key];

    Object.defineProperty(target, key, {
      set: (newValue: number) => {
        
        if (newValue < minNum) {
          throw new Error(`${key} is too small!`);
        }
        currentValue = newValue;
        
      },
      get: () => currentValue,
      configurable: true
    });
    } else if (possibleSetter){
      Object.defineProperty(target, key, {
        set: (newValue: number) => {
          
          if (newValue < minNum) {
            throw new Error(`${key} is too small!`);
          }
          possibleSetter!(newValue);
          
        },
        configurable: true
      });
    }
  }
    
}

I teraz możemy zamienić te dekoratory miejscami i sprawdzić, że oba działają:

@WithRender('ul-app')
class PersonItem {
    @required
    public name: string;
    // @aboveZero
   
    @min(1)
    @max(100)
    public age: number;
   
    constructor(name: string, age: number){
        this.name = name;
        this.age = age;
    }
    @enumerable(true)
    @deprecated
    sayHello() {
      console.log(`Hello, my name is ${this.name}!`);
    }
}

No cóż, mam nadzieję, że ten poradnik komuś pomoże.