Wykorzystujemy wiedzę o dekoratorach klasowych oraz OOP, aby napisać (dobrze) dekoratory frozen oraz final. Dodam, że kody przedstawiane w internecie nie działają tak, jak są przedstawiane. Zaczynajmy.
Ok, nasza klasa:
@frozen
class FreezeMe {
public name: string;
constructor(name: string){
this.name = name;
}
}
let frozenObj = new FreezeMe("John");
frozenObj.name = 'Jane';
console.log(frozenObj.name);
A teraz głupi dekorator, który wala się po internecie:
function frozen(target: Function): void {
Object.freeze(target);
Object.freeze(target.prototype);
}
I co? I nic nie działa! Ani nie jest frozen w TS, ani w JS. Ok, wywalamy ten dekorator, robimy readonly w TS:
class FreezeMe {
public readonly name: string;
constructor(name: string){
this.name = name;
}
}
let frozenObj = new FreezeMe("John");
frozenObj.name = 'Jane'; //pokazuje errora
console.log(frozenObj.name);
Ok, już teraz pokazuje errora. Swoją drogą na całej klasie readonly nie możemy zrobić, tylko na property. Dobra, ale teraz runtime, nie chcemy, aby dało się zmieniać name (ani cokolwiek innego).
Nic prostszego:
class FreezeMe {
public readonly name: string;
constructor(name: string){
this.name = name;
Object.freeze(this);
}
}
let frozenObj = new FreezeMe("John");
console.log(frozenObj.name);
Ok, ale my chcemy dekorator. To sobie napiszemy, patent już znamy, choć może wydawać się, że to nie wiadomo jak trudne:
function Frozen<T extends { new (...args: any[]): object }>(target: T): T {
return class extends target {
constructor(...args: any[]) {
super(...args);
Object.freeze(this);
}
}
}
@Frozen
class FreezeMe {
public readonly name: string;
constructor(name: string){
this.name = name;
}
}
let frozenObj = new FreezeMe("John");
console.log(frozenObj.name);
Readonly nam zapewnia, że w TS sobie tego nie nadpiszemy. Dekorator sprawia, że obiekt klasy jest frozen. Tu będę powtarzać do znudzenia, po pierwsze REPL kłamie, jak sprawdzimy, to się okaże, że nic nie zostało nadpisane. Po drugie, target w dekoratorze klasy to klasa, nie obiekt.
A my chcemy, żeby obiekt był frozen i to mamy za pomocą dekoratora. Te nowe dekoratory (bo zmieniło się ostatnio w TS) też ogarnę, podobnie jak reflect metadata, ale już teraz widzimy, że nawet stare dekoratory i czyste, bez żadnych biblioteczek, też mają sporo mocy i możemy to i owo nimi osiągnąć.
To spróbujmy final. Zaczniemy bez dekoratora, aby wyjaśnić o co chodzi z final:
//@final
class FinalMe {
public readonly name: string;
constructor(name: string){
this.name = name;
}
}
let finalObj = new FinalMe("John");
console.log(finalObj.name); //john
class FinalChild extends FinalMe {
public readonly age: number;
constructor(name: string, age :number){
super(name);
this.age = age;
}
}
let finalChildObj = new FinalChild("Jane", 20);
console.log(finalChildObj.name, finalChildObj.age); //jane 20
Czyli tak, jak widać tutaj coś sobie dziedziczy z tej klasy bez problemu. Ok, odkomentujemy dekorator i go napiszemy:
function final<T extends { new (...args: any[]): object }>(target: T): T {
return class Final extends target {
constructor(...args: any[]) {
if (new.target !== Final) {
throw new Error('Cannot inherit from final class');
}
super(...args);
}
};
}
Tutaj wykorzystaliśmy sztuczkę z nazwaniem zwracanej klasy. I już, nie można dziedziczyć. Dodam, nie można, ale to wychodzi w runtime, TS nie jest tak ogarnięty, aby przed kompilacją zauważyć, że coś jest nie tak, w końcu osiągamy to przez dekorator.
W internecie znalazłem też dekorator metody final (ponoć działa tylko jak transpilujemy do es5 lub wcześniejszych):
function final(target: Object, key: string | symbol, descriptor: PropertyDescriptor) {
descriptor.writable = false;
}
Teraz użycie w klasie rodzica:
class Parent {
@final
speak() {
console.log('Parent speaking');
}
}
A teraz klasa-dziecko:
class Child extends Parent {
// Error: "speak" is read-only
speak() {
console.log('Child speaking');
}
}
Ok, TS nie jest moim ulubionym tematem, nie lubię go jak jasna cholera, ale idziemy w dobrym kierunku i powoli to się staje jasne, co tu się wyczynia. W najbliższych lekcjach poznamy utility types, bardziej zaawansowane typy, nowe dekoratory, reflect metadata.
Wtedy w zasadzie będziemy mogli zaczynać zabawę w React/Ionic/Node+Express z TSem (takie prawdziwe Node+Express, z dekoratorami do metod jak we Flasku, bo doinstalować TDFa do node i może w metodzie/dwóch zrobić anotacje, to żadna sztuka i żaden TS).