Poznajemy bliżej dekoratory w TypeScript. Skupimy się na dekoratorach klas. Zaczyna robić się naprawdę ciekawie. Do dzieła.
Ok, zobaczmy co przyjmuje dekorator klasy:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
function stupidLog(constructor: Function){
console.log(`decorator called for ${constructor.name}`);
}
Cóż, są to funkcje konstruktora. To ciekawy przykład, bo idę o zakład, że żaden z tych dekoratorów nie zadziała tak, jak się tego spodziewamy. Najpierw jednak dopiszmy sobie klasę i poznajmy składnię:
@stupidLog
@sealed
class SealMe {
name: string;
constructor(name: string) {
this.name = name;
}
}
let seal1 = new SealMe("John");
let seal2 = new SealMe("Jane");
//decorator called for SealMe - 1 raz
console.log(Object.isSealed(seal1)); //false
console.log(Object.isSealed(seal2)); //false
Ok, czyli po pierwsze, stupidLog wywołał się jeden raz. Po drugie, żaden z obiektów nie jest sealed. Wytłumaczenie:
- StupidLog wywołałby się nawet, gdyby klasa była pusta. Bo ten dekorator chodzi wtedy, gdy jest klasa definiowana, a nie obiekt tworzony. Inaczej dekorator musiałby zwrócić nową klasę dziedziczącą z tej poprzedniej i dopisującą coś do konstruktora – po użyciu super
- sealed, cóż, musimy pamiętać, że klasa to funkcja-konstruktor i to ona – obiekt w JS jak wszystko inne – jest sealed. SealMe.prototype – tam nic nie dopiszemy. Do obiektów tak.
Mam też nadzieję, że ogarniamy, że wiele rzeczy TSa (interfejsy, prywatne/publiczne properties) istnieje tylko w TS. W JS nie ma tego.
Ok, bez zbędnego przedłużania, pokazujemy teraz przykład dekoratora, który może zwrócić klasę z jakimiś zmianiami:
function reportableClassDecorator<T extends { new (...args: any[]): {} }>(constructor: T) {
return class extends constructor {
reportingURL = "http://www...";
};
}
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
const bug = new BugReport("Needs dark mode");
console.log(bug.title); // Prints "Needs dark mode"
console.log(bug.type); // Prints "report"
// console.log(bug.reportingURL);
I tutaj ważna uwaga, konstruktora nie ruszyliśmy, to raz. Dwa, reportingURL będzie całkowicie niewidoczny dla TS, próba wypisania go wywoła błąd. Ale w JS (np. w konsoli w devtoolsach) już możemy się tym bawić.
Tym niemniej poznaliśmy patent, jak robić dekorator. Wystarczy trochę pokombinować i wpadniemy na coś takiego:
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
function stupidLog<T extends { new (...args: any[]): {} }>(originalConstructor: T){
return class extends originalConstructor {
constructor(...args: any[]){
super(args);
console.log("New object created");
}
}
}
@stupidLog
@sealed
class SealMe {
name: string;
constructor(name: string) {
this.name = name;
}
}
let seal1 = new SealMe("John");
let seal2 = new SealMe("Jane");
//New object created x2
console.log(Object.isSealed(seal1)); //false
console.log(Object.isSealed(seal2)); //false
Już chyba widzimy, co się tu dzieje. Ten console log odpalany jest teraz za każdym razem, gdy nowy obiekt jest tworzony. A jak sobie poradzić z sealowaniem nowych obiektów?
Do tego to nawet dekorator nie jest potrzebny:
@stupidLog
@sealed
class SealMe {
name: string;
constructor(name: string) {
this.name = name;
Object.seal(this);
}
}
let seal1 = new SealMe("John");
let seal2 = new SealMe("Jane");
//New object created x2
console.log(Object.isSealed(seal1)); //true
console.log(Object.isSealed(seal2)); //true
Żeby jednak te dekoratory miały jakikolwiek sens, powinny one przyjmować jakieś argumenty i coś konkretnego robić. Ok, napisałem taki przykład, postarajmy się jakoś to ogarnąć:
function WithRender(hookId: string) {
return function<T extends { new (...args: any[])}>(
originalConstructor: T
) {
return class extends originalConstructor {
constructor(...args: any[]) {
super(...args);
const hookEl = document.getElementById(hookId);
if (hookEl) {
let li = document.createElement("li");
li.textContent = `Name: ${this.name} Age: ${this.age}`;
hookEl.appendChild(li);
}
}
};
};
}
Tu jest kolejna warstwa abstrakcji, sam kod po ostatnim returnie raczej nie powinien przerażać, ale tak, ten hookId to w dekoratorze będzie argument. Zwracamy funkcję dekorator, która zwraca nową klasę, której konstruktor został wyposażony o nowe możliwości.
Dalej wygląda to tak:
@WithRender('ul-app')
class PersonItem {
public name: string;
public age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
}
let john = new PersonItem("John", 30);
let jane = new PersonItem("Jane", 20);
Czyli prosto. Ok, dekoratory trochę przypominają callback hell więc możemy choć trochę sobie to uprościć jeszcze:
function WithRender(hookId: string) {
return function<T extends { new (...args: any[])}>(
originalConstructor: T
) {
return class extends originalConstructor {
constructor(...args: any[]) {
super(...args);
this.mount();
}
mount(){
const hookEl = document.getElementById(hookId);
if (hookEl) {
let li = document.createElement("li");
li.textContent = `Name: ${this.name} Age: ${this.age}`;
hookEl.appendChild(li);
}
console.log("Mounted");
}
};
};
}
Podzieliliśmy to logicznie, bo de facto nie chcemy nic w konstruktorze zmieniać, poza odpaleniem mount i tyle. I jasne, można to napisać w klasie, bez dekoratora, natomiast całe piękno tego rozwiązania polega na tym, że my ten dekorator możemy podpinać do każdej klasy.
Więcej TSa niedługo!