Poznajemy dalej dekoratory w TypeScript. Nie jest to najłatwiejszy element języka, ale jakoś musimy się przez niego przegryźć. Do dzieła.

Ok, zrobimy sobie taką klasę:

class StupidClass {
  name: string;
  age: number;
  constructor(name, age){
    this.name = name;
    this.age = age;
  }
  // @enumerable(true)
  //   @deprecated
    sayHello() {
      console.log(`Hello, my name is ${this.name}!`);
    }
}

let stupidObj = new StupidClass("jim", 30);

for (const property in stupidObj) {
  console.log(`${property}: ${stupidObj[property]}`);
}

Ok, teraz dekorator metody:

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

Tutaj zmieniamy enumerable deskryptora na wartość przekazaną do dekoratora. Reszta (czyli value, którym jest funkcja) zostaje jak była. To teraz dekorator deprecated:


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;
}

Też chyba widać dokładnie co robi i powinniśmy rozumieć, co tu się dzieje. Ok, teraz możemy odkomentować dekoratory z „głupiej” klasy – będzie działać.

Dobra, teraz dodamy sobie do klasy z poprzedniej lekcji takie dekoratory:

@WithRender('ul-app')
class PersonItem {
    @required
    public name: string;
    @aboveZero
    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}!`);
    }
}

Ok, piszemy required:

function required(target: any, key: string) {
    let currentValue = target[key];
    
    Object.defineProperty(target, key, {
      set: (newValue: string) => {
        if (!newValue) {
          throw new Error(`${key} is required.`);
        }
        currentValue = newValue;
      },
      get: () => currentValue,
      enumerable: true,
      configurable: true
    });
  }

I tu warto dodać, że ten property dekorator to akurat by wyczyścił nam enumerable i configurable a i dostać się tam nie da (target to nie instancja tylko metoda) więc trzeba było się do czegoś takiego uciec, albo by nam to property się nie wyświetlało w for..in.

Podobnie wygląda dekorator aboveZero:

function aboveZero(target: any, key: string) {
    let currentValue = target[key];
   
    Object.defineProperty(target, key, {
      set: (newValue: number) => {
        
        if (newValue <= 0) {
          throw new Error(`${key} must be greater than zero`);
        }
        currentValue = newValue;
        
      },
      get: () => currentValue,
      enumerable: true,
      configurable: true
    });
  }

I powiem tak, to wszystko są trudne rzeczy, w dodatku aby dobrze sobie radzić z dekoratorami de facto trzeba by zainstalować biblioteczkę reflect metadata. W ogóle mało jest informacji i dobrych źródeł o nich a często czytając przykłady w internecie człowiek ma wrażenie, że Chat GPT to pisał albo ludzie, którzy sami nie bardzo wiedzą o czym piszą i trudno się będzie od nich czegoś specjalnie nauczyć.

Krótko mówiąc jest trudno, ale staramy się to mimo wszystko zrozumieć. Sam się przez to przebijam i bywa różnie. Więcej TS (dekoratorów mam nadzieję również) już niedługo.

EDIT: Podaję taki dekorator z argumentem:

 function max(maxNum: number) {
  return function(target: any, key: string) {
    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,
    });
  }
}

Ok, teraz użycie:

@WithRender('ul-app')
class PersonItem {
    @required
    public name: string;
    // @aboveZero
    @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}!`);
    }
}

Jest albo-albo. Nie można mieć dwóch bo mamy cannot redefine property. W sumie wynika to z kolejności, w jakiej są używane dekoratory (max najpierw) oraz configurable ustawionego domyślnie na false.

Ok, można to naprawić, napiszmy sobie takie dekoratory:

function max(maxNum: number) {
  return function(target: any, key: string) {
    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
    });
  }
}

function min(minNum: number) {
  return function(target: any, key: string) {
    let currentValue = target[key];
    console.log(target);
    Object.defineProperty(target, key, {
      set: (newValue: number) => {
        
        if (newValue < minNum) {
          throw new Error(`${key} is too small!`);
        }
        currentValue = newValue;
        
      },
      get: () => currentValue,
      configurable: true
    });
  }
}

I teraz mamy taką klasę:

@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}!`);
    }
}

Dobrze, że choć rozumiemy kolejność, czyli najpierw min aplikowany, potem max, a jako że configurable na true to jest możliwe. Problem w tym, że ten setter zostanie nadpisany przez max i ten min przestanie działać.

Więc rozjeżdża nam się to wszystko. Może reflect metadata jest jakimś rozwiązaniem, postaram się ogarnąć biblioteczkę i zauważyć. Na razie widzimy, że nie bardzo idzie te dekoratory stackować jedne na drugich.