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.