TypeScript. Tworzenie modułu Node.js

W pracy przerzucam się ostatnimi czasy na Javę. W PHP dawno nic mądrego nie pisałem, a JavaScript, jak bardzo bym go nie kochał, mimo wszystko zaczyna mnie trochę uwierać z uwagi na brak silnego typowania ani nawet czegoś tak podstawowego jak type hinting. Jasne, zawsze można stosować tzw. duck typing, czyli ręczne sprawdzanie typu argumentu przekazanego do funkcji, ale to ani nie rozwiązuje wszystkich problemów, ani nie jest wygodne, ani tym bardziej eleganckie. Pewne postępy w tej materii poczynił EcmaScript w wersjach 6 i 7. Miałem okazji używać ES7 przy okazji zabaw z frameworkiem Aurelia. Ciekawą alternatywą okazał się jednak TypeScript.

Od razu powiedzmy sobie jedno: fakt, że TypeScript oferuje silne typowanie nie oznacza, że jest ono transpilowane do wynikowego kodu JS. Jeśli zatem napiszemy moduł w TS, a następnie użyjemy go jako zależności w innym, napisanym w JS, to nadal będziemy w stanie przekazać do dowolnej funkcji takie argumenty, jakie uznamy za stosowne. Kod wynikowy i tak będzie się uruchamiał jako zwykły JS, a zalety typowania zobaczymy głównie w środowisku programistycznym. Jednak na etapie transpilacji (TypeScript uparcie nazywa to „kompilacją”, pomimo, że jedynie transpiluje z jednego języka na inny, a kompilacja, a w zasadzie to interpretacja i tak zachodzi dopiero w środowisku wykonawczym) można uniknąć tego, co w Javie byłoby błędem kompilacji. Nasz kod zgłosi błędy jeśli pomieszamy typy, czy przekażemy do danej funkcji niewłaściwą liczbę argumentów.

Jak zatem zabrać się do stworzenia modułu w TS, tak, by można było go potem użyć jako zależności w innym module? Zaczynamy od zainstalowania TypeScriptu:

npm install -g typescript

Następnie tworzymy katalog z naszym modułem, wchodzimy do niego i inicjalizujemy projekt TS:

tsc --init

To polecenie stworzy nam plik tsconfig.json. Jest on dość podstawowy, więc musimy ręcznie go podrasować. Schemat tego pliku można znaleźć tutaj, zaś listę opcji kompilacji można obejrzeć tutaj - jak ktoś chce sę wgłębić i poeksperymentować, to zachęcam do zabawy. Na potrzeby naszego prostego modułu będziemy chcieli jednak uzyskać coś takiego:

{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "moduleResolution": "node",
        "declaration": true,
        "outDir": "dist/"
    },
    "files": [
        "src/greeter.ts"
    ]
}

W podanym wyżej opisie schematu można znaleźć dokładne informacje o tym, co każda z opcji robi. Kilka słów dotyczących opcji kompilatora:

  • Opcja target mówi, do jakiej wersji EcmaScript chcemy transpilować kod TS. Tu zawsze będzie wartość "es5" - przynajmniej dopóki ES6 nie stanie się obowiązującym, wszędzie zaimplementowanym standardem.
  • Opcja module opisuje, w jaki sposób tworzony jest kod modułu. Dla Node.js powinniśmy używać wartości "commonjs", gdyż wygeneruje nam ona kod korzystający z wbudowanego w Node.js mechanizmu ładowania modułów za pomocą funkcji require().
  • Opcja declaration stworzy nam dodatkowo pliki typu .d.ts. Są one potrzebne w momencie, gdy nasz moduł będzie wykorzystywany jako zależność innego, również napisanego w TypeScripcie.
  • W końcu outDir, czyli katalog, do którego zapisywane będą wynikowe pliki JS.

Tworzymy plik src/greeter.ts, który podaliśmy jako plik wejściowy. Jeśli plik importuje inne, to one również zostaną przetworzone. Na razie nie przejmujmy się jednak importami. Stwórzmy prostą klasę, którą można będzie wykorzystać w innym module:

export class Greeter {
    makeGreeting(name: string): string {
        return `Hello ${name}`;
    }
}

Aby całość przetworzyć, wystarczy wywołać następujące polecenie:

tsc

Stwórzmy jeszcze plik package.json:

npm init

Na koniec zadbajmy o następujące właściwości pliku package.json:

{
    "main": "dist/greeter.js",
    "typings": "dist/greeter.d.ts"
}

O ile właściwość "main" jest oczywista i wskazuje na główny plik modułu (nie ma to nic wspólnego z TypeScriptem), o tyle "typings" to coś niestandardowego. Podając opcje kompilacji, wybraliśmy opcję generowania informacji o typach ("declaration": true). Wskazany tu plik to właśnie plik wynikowy utworzony dzięki tej opcji, zawierający informacje o typach. Dzięki temu możliwe jest używanie naszego modułu w innych projektach, również pisanych w TypeScripcie.

I to by było na tyle. Wystarczy, że nasz moduł znajdzie się w katalogu node_modules innego projektu. Zakładając, że nazwaliśmy go np. „greeter”, w nowym module możemy zrobić coś takiego:

import {Greeter} from "greeter";

var greeter = new Greeter();
console.log(greeter.makeGreeting("world"));

Łatwe? Łatwe!


Komentarze

Andrzej

2016-01-04, 09:06

Może wyjdę na ignoranta, ale jak można popełniać takie błędy jak wysyłanie do funkcji argumentów o złych typach? Jeśli to Twoja własna funkcja to zaglądasz w źródło i wiesz co ma być. Jeśli to obcy kod to masz dokumentację.

Moim zdaniem brak kontroli typowania to świetna sprawa, bo masz jedną funkcję, która w zależności od typu może działać nieco inaczej. Normalnie trzeba by pisać kilka podobnych funkcji. A w przypadku jakiejś zmiany wszędzie nanosić poprawki. Dzięki braku kontroli łatwiej zachować zasadę DRY. No ale to moje zdanie, nie jestem informatykiem z wykształcenia :)

A do opanowania tego wystarczy jakaś odmiana notacji węgierskiej.

Ja osobiście popełniam jeden błąd wynikający z przyzwyczajenia do PHP, gdzie w samej definicji funkcji można w parametrach wstępnie zdefiniować wartość np. bFlag=false, czego w JS zrobić nie można (to jedyny znany mi język z takim babolem).

Dominik Marczuk

2016-01-04, 11:13

No to mało języków znasz :). Generalnie w językach z silnym typowaniem zetknąłem się właśnie z podejściem stawiającym na tworzenie kilku metod o tej samej nazwie, z których każda ma inny zestaw argumentów. Zasada DRY nie musi być łamana, metody te wszak mogą wołać się wzajemnie, a plus dla programisty jest taki, że ma konkretne sygnatury metod i wie, że może je zawołać z zestawem argumentów A, B, ale już nie C. No i zachowana jest wysoka czytelność kodu, co w przypadku wielu możliwych typów argumentów i duck typingu jest problemem.

Słabe typowanie w JS ma oczywiście swoje plusy, tu się nie sprzeczam, jednak ma też minusy. Ot, chociażby takie obrzydliwe triki jak notacja węgierska. Nazwa zmiennej ma wskazywać na przeznaczenie tej zmiennej, a nie na jej typ. Typ podpowiedzieć powinno IDE - po to właśnie ono jest. No i zauważ, że notacja węgierska w JS nie ma sensu, jest tylko potencjalnym źródłem błędów. Masz zmienną o nazwie bFlag, której wartość zwraca funkcja. Ale w tej funkcji w pewnym momencie np. użyjesz sobie operatora bitowego & - efekt jest taki, że funkcja zacznie zwracać Number zamiast Boolean. Błąd zauważysz prawdopodobnie dopiero w jakiejś skrajnej sytuacji pół roku później, kiedy coś się sypnie "bez powodu".

Co do Twojego argumentu z dokumentacją, zapytam tylko: co wolisz robić, pisać nowy kod, czy grzebać po dokumentacji? TypeScript (C++, Java...) sprawia, że nie musisz tego robić, bo IDE podpowie Ci to, czego normalnie szukałbyś po dokumentacji albo po innych plikach swojego kodu. Wbrew pozorom, to mocno usprawnia pracę. Przynajmniej ja mam takie odczucie. Wyobraź sobie sytuację, że nawet we własnym kodzie masz metodę, która zwraca dane jakiegoś typu. Wykorzystujesz ją w wielu miejscach. W pewnym momencie zmieniasz sygnaturę tej metody. JavaScript nic Ci nie podpowie w związku z tym i będziesz musiał ręcznie poprawić wszystkie wywołania tej metody. TypeScript od razu znajdzie Ci wszystkie wywołane w ten sposób błędy. Może to skrajna sytuacja, ale mnie się zdarzyła. A nie musiała.

Andrzej Kidaj

2016-01-04, 12:36

Spotkałem się z tym na pewno w C/C++ i prawdę mówiąc niezbyt mi się to podobało. Wiem, że jestem dziwny, ale dla mnie czytelność kodu to mała ilość kodu. Dlatego też podoba mi się brak typowania w PHP, na większość właśnie się uskarża :)

Co do notacji węgierskiej, to pewnie też jestem freakiem, ale dla mnie jest super. Chociaż faktycznie wprowadza w błąd, kiedy w parametrze funkcji mam aTablica lub iID a zamiast tablicy mogę przesłać jeden ID a zamiast iID całą ich tablicę. No ale tu się świetnie sprawdza komentarz przy funkcji.

Wiesz, ja jestem pewnie wyjątkiem, bo koduję sam i wiem, co mam napisane. Jeśli chodzi o dokumentację, to mi wystarczy 2-zdaniowy opis funkcji i ewentualnie przykład (z tego korzystam w przypadku jQuery), ewentualnie komentarz przy danej funkcji. Do swojego CMSa napisałem krótką dokumentację, której wydruk mam zawsze pod ręką.

Swego czasu mocno przebudowywałem i ujednolicałem kod CMS-a (PHP o JS), ale nie miałem opisanej przez Ciebie sytuacji. Przyznaję, zdarzają mi się błędy, które wyskakują po jakimś czasie, ale akurat z nieco innych powodów. Raczej to były typowe błędy, które ZAWSZE się pojawiają w rozbudowanym kodzie.

No, ale jak wspomniałem, ja jestem dziwny. I tak jak kiedyś kochałem C/C++ za swobodę programowania (czego nie daje np. Java), tak kocham PHP i JS za jeszcze większą swobodę, chociaż już mniej za wydajność :)

Dominik Marczuk

2016-01-04, 12:51

Sam zatem wskazujesz sytuację, w której notacja węgierska wprowadza w błąd :)

Komentarze zaś to kolejne źródło błędów i nieporozumień, bo trzeba je utrzymywać równolegle z rozwojem kodu ("dobra, najpierw napiszę do końca ten kawałek, potem się zaktualizuje docblock"). Efekt jest taki, że im komentarz jest starszy, tym bardziej się desynchronizuje z rzeczywistością. Nie ufam komentarzom o ile tylko mam solidniejszą alternatywę (np. określony na sztywno typ, który mi podpowie IDE). I sam też komentarze coraz rzadziej stosuję właśnie na rzecz kodu, który jest tak prosty i czytelny, że sam sobie jest dokumnetacją. Bo jak masz jednolinijkowca, to i tak patrzysz raczej na kod niż na komentarz, który albo wprowadza w błąd, albo powtarza to, co i tak widzisz w kodzie na pierwszy rzut oka.

Polecam "Czysty kod" Roberta C. Martina (nawet można sobie znaleźć PDF do pobrania za darmoszkę w Internetach, co prawda w oryginale, ale i tak czyta się super). Książka traktuje między innymi właśnie o tego typu kwestiach. Fakt, że dotyczy głównie Javy, ale praktycznie wszystko, co w niej zawarte, daje się odnieść do JavaScriptu. Moje rozumienie czystości i czytelności kodu wzięło się w dużej mierze właśnie z tej książki (plus paru innych, ale ta jest moim zdaniem pozycją zdecydowanie najważniejszą).

Andrzej

2016-01-04, 13:10

Zgadzam się, że notacja węgierska w tym wypadku może się nie sprawdzić.

Jeśli chodzi o komentarze, to tutaj znowu wychodzi moja odmienność. Generalnie stawiam na minimalistyczny i samokomentujący się kod (jak Ty) i u mnie znajdziesz ich bardzo niewiele. W zasadzie tylko w miejscach, które mogą być potencjalnym źródłem problemów, albo kiedy stosują jakaś "dziwną" konstrukcję, z której nie zawsze wynika efekt (jest to też dla mnie informacja, żeby kiedyś to poprawić jak będę mądrzejszy). Albo właśnie kiedy parametry mogą być inne niż wynika z ich nazwy.

Poszukam "Czystego kodu". Ze swojej strony polecam "Wysokie C" Marka Kotowskiego. Możliwe, że porusza podobne tematy. Chociaż ja jestem praktykiem i robię tak, żeby mi było wygodnie a nie jak nakazuje "sztuka programowania". Dzięki temu, że pracuję sam, mogę sobie na to pozwolić :)

Dominik Marczuk

2016-01-04, 13:27

Poszukam, dzięki za rekomendację :).


Napisz komentarz


Szukaj wpisów


Chmura tagów