Mechanizmy TypeScript które powinieneś znać

mechanizmy_typescript
Zdjęcie autorstwa JOHN TOWNERa

TypeScript nieodwracalnie zmienił środowisko JavaScriptowe: połączył elastyczność i wszechobecność JavaScriptu, z wygodą programowania w języku silnie typowanym. Dzisiaj opowiem Ci o paru zaskakujących, bardziej zaawansowanych mechanizmach TypeScript’a, których znajomość pozwoli ci uniknąć błędów.

Mergowanie deklaracji

Przyjrzyjmy się poniższemu przykładowi. Mamy w nim dwa interfejsy o tej samej nazwie oraz klasę implementującą ten interfejs. W większości języków programowania poniższy zapis spowodowałby błąd kompilacji. Co się stanie w TypeScriptcie? Wyrzuci błąd? Drugi interfejs nadpisze pierwszy? A może coś innego? 

interface IBookISOFormat {
    title: string;
    format: string;
}

interface IBookISOFormat {
    content: string;
}

class Book implements IBookISOFormat {
    public title: string;
    public format: string;
    public content: string;
    constructor() {
            this.content = 'CONTENT';
    }
}

Pewnie część z Was będzie zaskoczona, że interfejs połączył swoje definicje. IBookISOFormat będzie miał trzy właściwości: title, format oraz content a jego poprawna implementacja musi mieć wszystkie trzy właściwości. Co się dzieje?

Nie jest to jednak koniec niespodzianek, to samo dzieje się jeśli spotka się klasa i interfejs o tej samej nazwie.


interface Book {
   content: string;
}
 
class Book {
   public title: string;
   constructor() {
       this.title = "TS"
       this.content = 'TypeScript is great';
   }
}
 
const d = new Book();
console.log(d.content); // ok

Jest to poprawne zachowanie, opisane w dokumentacji jako mergowanie deklaracji.
Temat jest dużo bardziej skomplikowany bowiem mergują się nie tylko interfejsy i klasy, ale także przestrzenie nazw, enumy, a nawet wartości… i to nie każdy z każdym. 

Kompatybilność strukturalna

W większości języków programowania rzutowanie na dany typ zawiera sprawdzenie czy i jak elementy są ze sobą powiązane np.: czy klasa implementuje dany interfejs albo czy klasy są “spokrewnione”. Wszystkie relacje wynikają ze struktury kodu.

TypeScript kieruje się jednak kompatybilnością strukturalną, czyli porównuje strukturę obiektu. Spójrzcie na przykład poniżej, ilustruje on bardzo prosty przypadek:

interface IBookISOFormat {
    name: string;
}

class Book {
    name: string;
}

const p: BookISOFormat = new Book();

Jeśli interfejs IBookISOFormat i klasa Book mają właściwość name to znaczy, że spokojnie można przypisać zmienną typu Book do IBookISOFormat. I na odwrót.

Co więcej nie musi być to identyczna struktura, wystarczy, że typ przypisywany ma co najmniej wszystkie właściwości typu na który jest – upraszczając – rzutowany. Na przykład:

class Dog {
    name: string;
    age: number;
}
 
class Book {
    name: string;
}
 
const a: Book = new Dog(); // ok
 
const b: Dog = new Book(); // error

Pies może stać się książką, bo książka ma tylko właściwość name, jednak w drugą stronę to nie zadziała, gdyż pies posiada właściwość age. Albo jeszcze inaczej: struktura książki zawiera się w strukturze psa.

To tylko zalążek problemu z kompatybilnością strukturalną, jeśli Was to zainteresuje to poświęcę temu cały artykuł. 

Teraz weźmy głęboki oddech i spróbujmy czegoś prostszego.

Pola prywatne w klasach

Kolejna ciekawostka dotyczy pól prywatnych w klasach. Do wydania wersji 3.8 (która jest najnowszą wersją w chwili pisania tego artykułu) TypeScript nie posiadał pól prywatnych w klasach. To znaczy posiadał, ale po transpilacji do Javascriptu wszystkie pola były dostępne publicznie. Z powodu budowy klas w Javascriptcie we wcześniejszych wersjach TypeScripta znaczniki zasięgu zmiennych były tylko informacją dla developerów oraz interpretera. Nie istniała techniczna przeszkoda – poza niezadowoleniem linterów oraz kolegów z zespołu – żeby dostać się do prywatnego pola w klasie.

W końcu twórcy wprowadzili prawdziwe pola prywatne do TypeScripta, niestety twórcy zdecydowali się na kompatybilność z ECMAScript i są one po… hashu.

To się poprawnie wywoła, chociaż nie powinno:

class Book {
    private name: string;

    constructor(name: string) {
            this.name = name;
    }
}

const myBook = new Book("TypeScript is the best");

myBook.name; // ok

A to zwróci błąd jak oczekiwaliśmy:

class Book {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }
}

const myBook = new Book("TypeScript is the best");

myBook.#name; // error

Zmienna po hashu nie jest widoczna po transpilacji do JavaScriptu, nie jest widoczna w prototypie i nie da się do niej dostać poza klasą.

Ten przykład doskonale pokazuje jak TypeScript jest i będzie uzależniony od EMCAScript oraz jak łatwo popełnić błąd bez znajomości TypeScripta.

Podsumowanie

Mam nadzieje, że jeśli kiedyś zobaczycie w podpowiedziach właściwość której nie ma w definicji klasy, pomyślicie o tym artykule i przez głowę przejdzie Wam fraza “mergowanie deklaracji”. I że będziecie patrzyli na rzutowanie typów z zdrową podejrzliwością z powodu kompatybilności strukturalnej. A co najważniejsze będziecie pisali kod świadomie i pewnie, bo chociaż robię to codziennie, TypeScript nigdy nie przestaje mnie zadziwiać.

A wy jakie znacie dziwactwa TypeScripta? Które kochacie a których nienawidzicie? Czy łatwo się jest przesiąść z innego silnie typowanego języka na TypeScripta?

Podzielcie się swoimi doświadczeniami w komentarzach.

Z zawodu i z pasji programistka JavaScript, ze słabością do TypeScripta. Nieustannie poznaje nowe technologie, uwielbia pisać i chętnie się dzieli wiedzą. Spełnia się jako mentor oraz twórca internetowy, autorka solutionchaser.com. Prywatnie lubi dobrą herbatę, rozwiązywać problemy i gry komputerowe (szczególnie RPG). Można ją znaleźć na Instagramie. Uważa, że nic nie rozwija tak jak bugi i dobry zespół :) Życie to najlepsze co jej się przytrafiło.
PODZIEL SIĘ