Gdy pisałem moją wrześniową grę co miesiąc, nie znałem jeszcze dobrego sposobu na modularyzację kodu w TypeScript. Gra nie była specjalnie duża ale i tak brak modułów spowodował, że kod był cięższy do czytania i utrzymania.
Dziś przedstawię rozwiązanie tego problemu. Będą to podstawowych technik wykorzystania TSowych modułów. Na początek opiszę idealnie nadające się do dzielenia kodu na utrzymywalne i czytelne fragmenty – przestrzenie nazw czyli znane wszystkim użytkownikom Javy, C++ czy C#, namespace`y.
Aby stworzyć przestrzeń nazw w TypeScript wystarczy użyć operatora namespace, po którym podajemy nazwę tworzonego modułu i parę nawiasów klamrowych, które zawierać będą kod, który chcemy enkapsulować. Oto przykład:
1 2 3 |
namespace myModule { } |
W nazwach tak tworzonych modułów, możemy używać kropek co od razu budje odpowiednią hierarchie przestrzeni nazw:
1 2 3 |
namespace app.utilities.dataParser { } |
Nawet jeśli wcześniej nie określiłem żadnej z tych przestrzeni, ten kod zadzaiała. Automatycznie tworzę trzy zagnieżdżone moduły: dataParser, który zawiera się w utilities, który z kolei jest częścią większego modułu app. Oczywiście moduły mogą otrzymywać dowolne nazwy zgodne z JS’ową konwencją nazywania zmiennych.
Spójrzmy na konkretniejszy przykład. Stworzę przestrzeń nazw classes, w której zadeklaruję nową klasę:
1 2 3 4 5 6 7 |
namespace classes { class Dog { constructor(public name:string){}; paws:number = 4; sound:string = "Woof!" } } |
Klasa Dog, nie będzie teraz dostępna poza przestrzenią nazw. Można się do niej odwoływać tylko wewnątrz classes. Ten kod, wpisany pod przestrzenią nazw, wywoła błąd:
1 |
var maja = new Dog("maja") |
Dog nie jest dostępne w globalnym zakresie. Edytor zgłosi po prostu błąd, że zmienna Dog nie została zadeklarowana.
W zwykłym JavaScripcie, aby dostać się do przestrzeni nazw (która zazwyczaj jest po prostu zwykłym obiektem), wystarczyło użyć notacji z kropką. W taki sposób:
1 |
var maja = new classes.Dog("maja") |
W TypeScript także musimy to zrobić. Program musi wiedzieć z jakiej przestrzeni nazw chcemy skorzystać. W końcu dwie różne przestrzenie, mogą mieć w sobie dane o takich samych identyfikatorach.
Jednak edytor wciąż zgłasza błąd:
Błąd mówi, że w classes nie istnieje zmienna o nazwie Dog. Oczywiście to nie prawda. Jednak TypeScript jest na tyle mądry, że nie pozwala wyciekać z modułów wszystkiemu jak leci (co jest problemem w zwykłym JS). Jeżeli chcemy aby jakaś część modułu była dostępna po za nim, musimy wyraźnie to wskazać. Aby to zrobić, wystarczy przed interesującym nas elementem dopisać export:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
namespace classes { export class Dog { constructor(public name:string){}; paws:number = 4; sound:string = "Woof!" } class Cat { constructor(public name:string){}; paws:number = 4; sound:string = "Meow!" } } var maja = new classes.Dog("maja"); var filemon = new classes.Cat("filemon"); // zgłosi błąd |
Teraz klasa Dog jest eksportowana poza przestrzeń nazw. Pierwsza deklaracja zmiennej pod classes nie generuje już błędu. Druga za to tak, ponieważ klasa Cat nie jest dostępna poza modułem (brak export).
Jeżeli przestrzeń nazw jest zagnieżdżona w innych przestrzeniach, aby dostać się do niej z zewnątrz, należy po kropkach wymienić wszystkie przestrzenie patrząc „od góry”, czyli od zewnątrz. Zupełnie jak z obiektami w JavaScript.
Jeśli na przykład chcę wyłuskać pole z takiej przestrzeni:
1 |
namespace app.utilities.dataParser{} |
Musiałbym wpisać:
1 |
app.utilities.dataParser.interesujaceMniePole; |
Ale z poziomu modułu app, wystarczyłoby już tylko:
1 |
utilities.dataParser.interesujaceMniePole; |
Co ciekawe, nic nie stoi na przeszkodzie aby używać elementów jednej przestrzeń wewnątrz drugiej, niezależnej przestrzeni. Pod warunkiem oczywiście, że elementy te są najpierw eksportowane:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
namespace app.GUI.interfaces { export interface animal { paws: number; sound: string; name: string; } } namespace classes { export class Dog implements app.GUI.interfaces.animal { constructor(public name:string){}; paws:number = 4; sound:string = "Woof!" } } var maja = new classes.Dog("maja"); |
Tutaj, wewnątrz modułu classes odwołuje się do interfejsu animal z wnętrza przestrzeni interfaces, która z kolei znajduje się w GUI, siedzącego w app. Może ten przykład jest mało praktyczny, ale technicznie nic nie stoi na przeszkodzie aby stworzyć taki mechanizm.
Długie przestrzenie nazw mogą być kłopotliwe, zwłaszcza jeśli odwołujemy się do nich wiele razy w innym modle. Na szczęście jest sposób i na to. Wewnątrz aktualnego zakresu zmiennych, możemy stworzyć coś jakby alias dla długich przestrzeni. Robi się to za pomocą operatora import:
1 |
import animal = app.GUI.interfaces.animal; |
Po import podaję alias do którego chcę przypisać przestrzeń lub nawet konkretny jej element. Następnie stawiam znak równości i długą przestrzeń nazw.
W powyższym przykładzie wyglądałoby to tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
namespace app.GUI.interfaces { export interface animal { paws: number; sound: string; name: string; } } namespace classes { import interfaces = app.GUI.interfaces export class Dog implements interfaces.animal { constructor(public name:string){}; paws:number = 4; sound:string = "Woof!" } } var maja = new classes.Dog("maja"); |
Tworzę alias tylko do interfaces, na wypadek jakby zawierał jakieś elementy oprócz animal, które mnie interesują. Jak widać w niczym to nie przeszkadza. Tam gdzie wcześniej odwoływałem się do pełnej ścieżki przestrzeni nazw, teraz wpisuję tylko alias a po jego nazwie mogę normalnie używać notacji z kropką aby dostać się do znajdujących się głębiej elementów.
To wszystko jeśli chodzi o podstawy przestrzeni nazw w TypeScript, jednak nie wszystko jeśli chodzi o modularyzację kodu. Do tematu powrócę w kolejnym poście z serii.
Jeżeli chcesz być na bieżąco z postami na blogu zachęcam do polubienia mojej strony na facebooku. Zawsze zamieszczam tam informacje o wszystkich nowościach. Jest to też dobre miejsce na kontakt ze mną. Na wszystkie pytania zawsze odpowiem :).