Techniki Zaawansowane
2 godz. 35 min · Typescript · Full-stack i Programowanie
Adam GospodarczykZacznijmy od tego, że JavaScript jest dynamicznie typowany. Oznacza to, że typy zmiennych mogą się zmieniać w czasie wykonywania programu, a my nie mamy nad nimi kontroli. A po co nam ta kontrola? Między innymi po to, aby przypadkowo nie dodać typu number do string lub przekazać do funkcji nieprawidłowe danych. Ogromną zaletą jest to, że o tego typu błędach dowiadujemy się już na etapie pisania kodu a nie dopiero w trakcie jego wykonywania... na produkcji.
TypeScript umożliwia stosunkowo prostą migrację istniejących projektów JavaScript. W kursie na przykładzie zostały omówione trzy strategie, dzięki którym jesteśmy w stanie dodać statyczne typowanie do istniejącego kodu. W procesie migracji najważniejsze jest to, że przepisywanie kodu jesteśmy w stanie uzależnić również od poziomu naszej wiedzy na temat TypeScriptu i stopniowo dodawać kolejne elementy.
Na pierwszy rzut oka można uznać, że TypeScript tylko dodaje nam pracy, wymuszając dodatkową konfigurację oraz opisywanie typów. Mało tego zwiększa barierę wejścia w projekt dla mniej doświadczonych programistów. Trudno temu zaprzeczyć ALE - korzyści płynące ze statycznego typowania które wpływają na zmniejszenie liczby błędów oraz wykrywanie ich nawet na poziomie pisania kodu, oszczędzają czas który normalnie poświęcilibyśmy na późniejsze debugowanie aplikacji. Poza tym zwiększenie stabilności aplikacji daje szerokie korzyści biznesowe, które wielokrotnie wynagradzają nam czas wymagany na pisanie dodatkowej składni.
JavaScript jest językiem dynamicznie typowanym. W trakcie wykonywania programu, zmienne mogą zmieniać swój typ a my nie zawsze mamy nad tym świadomą kontrolę. Tylko ... po co nam ta kontrola? Główną zaletą statycznego typowania jest to aby uniknąć sytuacji w których przypadkowo przypisujemy tekst do zmiennej która powinna zawierać liczbę lub sytuacji w której przekazujemy do funkcji nieprawidłowe dane. Taki rodzaj pomyłek generuje błędy odniesienia (eng. ReferenceError), które zatrzymują działanie skryptu i są momentami bardzo uciążliwe do wykrycia. Dzięki statycznemu typowaniu, jesteśmy w stanie wykrywać je na etapie pisania kodu.
TypeScript na przestrzeni ostatnich lat zyskuje ogromną popularność w społeczności programistów JavaScript i coraz częściej jest nieodłącznym elementem ofert o pracę. W przypadku firm nastawionych na rozwój rozbudowanych aplikacji JavaScript już teraz jest w zasadzie standardem. Biorąc pod uwagę fakt, że najnowsze frameworki (np. Nest.js) wykorzystują TypeScript domyślnie, nauka tej technologii ma ogromny sens w kontekście rozwoju jako programista. I ostatnim argumentem za tym aby wziąć TypeScript na poważnie jest to, że stoi za nim firma Microsoft a on sam rozwijany jest już niemal 10 lat.
Ten kurs został stworzony z myślą o programistach, którzy podstawy TypeScriptu mają już za sobą i chcą poznać jego zaawansowane techniki. Aby w pełni wykorzystać wiedzę z tego kursu, niezbędna jest również zaawansowana znajomość JavaScriptu a w szczególności jego elementów prowadzonych w wersji ES6+. Zdecydowanie polecamy przerobienie kursu Podstawowego TypeScript, który znajdziesz na eduweb, przed korzystaniem z tego materiału.
3.9+
Kolejnym tematem które chciałbym poruszyć w tym kursie to tzw.
Function overload czyli inaczej mówiąc przeciążenia funkcji.
Chodzi o to że czasem jedną i tę samą
funkcję chcemy zadeklarować na kilka różnych sposobów.
Konkretnie mam tu na myśli sytuacje w których jedna funkcja przyjmuje różne
argumenty lub nie wymaga od nas tego abyśmy podali od razu wszystkie argumenty.
W samym Javascript nie mamy możliwości deklarowała wielu różnych funkcji
natomiast w przypadku TypeScriptu sytuacja wygląda nieco inaczej.
Warto jednak już na tym etapie zaznaczyć
że przeciążenie funkcji nie polega na tworzeniu dwóch różnych funkcji tylko
raczej na określaniu dwóch różnych typów tej samej funkcji.
Najlepiej będzie jednak jak pokażę Ci to na przykładzie
stawiam przed nami teraz następujący problem chcemy napisać funkcję która
będzie wykonywała metodę uppercase ma ciągu znaków lub tablicy ciągu znaków.
Teoretycznie taka funkcja mogłaby wyglądać tak że przy jej deklarowaniu
przyjmowaliśmy dane dowolnego typu a następnie wykonywali na nich metodę.
toUpperCase. Oczywiście takie coś nie do
końca nam tutaj zadziała bo wystarczy że ze względu na to any przekażemy na
przykład tablicę liczb. Chociaż z drugiej strony nawet jeżeli tutaj byłby tekst to
dalej nasza funkcja nie zadziała ze względu na to że wykonała by metodę toUpperCase,
bezpośrednio na tablicę a nie na jej elementach.
Drugim rozwiązaniem które przychodzi do głowy jest zastosowanie wiedzy z
poprzedniego filmu i zastąpienie typu any typem generycznym.
Na ten moment wystarczy że skorzystamy tutaj z union type.
W takiej sytuacji nasza funkcja zwróci string lub tablicę stringów.
W tym momencie musimy również zmienić
implementację naszej funkcji w taki sposób aby obsługiwała poszczególne typy.
Więc jeżeli przekazane dane są typu string zwracamy to co mieliśmy wcześniej.
W przeciwnym razie mamy tutaj do czynienia z tablicą.
W związku z tym musimy wykonać metodę map w taki sposób aby wykonać metodę toUpperCase,
na każdym z elementów tej tablicy.
No i w tej chwili mamy już sytuację o którą dokładnie nam chodziło.
Tutaj jeżeli zamienia argument tej funkcji na tablicę z stringów to otrzymam
poprawny wynik i tak samo w sytuacji gdyby tutaj znalazły się np.
moje imię.
Również wszystko byłoby w porządku.
Ostatecznie jednak w momencie gdy jedziemy na tę funkcję widzimy że mamy tutaj do
czynienia ze stringiem lub tablicą stringów a następnie otrzymujemy string lub
tablicę stringów. W tym konkretnym momencie.
Moglibyśmy to jeszcze zaakceptować no ale w sytuacji gdybyśmy mieli do czynienia z
większą liczbą typów bądź na przykład jakimiś klasami interfejsami itd.
Nagle okazałoby się że tego typu
informacja nie jest do końca dla nas czytelna.
Chciałbym tutaj dojść do momentu w którym przekazując do funkcji string otrzymuję
jasną informację o tym że z powrotem otrzymamy również string a nie np.
tablicę stringów. Aby sobie z tym poradzić.
Pierwszym takim najprostszym rozwiązaniem byłoby rozbicie tej funkcji na dwie.
Jedna z nich obsługiwała by tekst a druga tablicę słów.
Natomiast takie funkcje musiałyby się od
siebie różnić nazwą bo jeżeli teraz zadeklaruje funkcję o tej samej nazwie to
oczywiście mamy tutaj błąd wynikający z tego że nie możemy jednak tego zrobić.
Powiedziałem jednak, że w TypeScript coś takiego jest możliwe i faktycznie jest to
prawda ale na etapie określania typu a nie implementacji.
Więc jeżeli teraz usuniemy ciało tej funkcji
a następnie otworzymy kolejną deklarację obsługującą ten drugi przypadek no to
nagle okazuje się, że żadnego błędu tutaj nie ma.
I to nawet pomimo tego że te nazwy są dokładnie takie same.
W sytuacji gdy przekazujemy do tej funkcji
tablicę z treningów mamy tutaj jasną informację o tym że ta funkcja zwróci
również tablice ciągów znaków. I jednocześnie w momencie gdy przekazujemy
do tej tablicy z string taką samą informację otrzymujemy również z powrotem.
I teraz tak samo w tej drugiej sytuacji w
momencie gdy przekazujemy do funkcji string również mamy jasną informację o tym że
otrzymamy w zamian string. I to czego tutaj doświadczyliśmy.
To właśnie mechanizm przeciążenia funkcji.
Czyli zadeklarowaliśmy jedną funkcję na wiele różnych sposobów a konkretnie
określiliśmy różne typy tej samej deklaracji którą mamy tutaj.
Dodatkowo dzięki odpowiedniej
implementacji czyli wykorzystaniu tutaj union type oraz mechanizmu type guard o
którym będziemy jeszcze mówić nagle okazało się, że TypeScript
stał się zdecydowanie mądrzejszy w kontekście rozpoznawania tego jakie dane
trafiają do funkcji oraz jakiego rodzaju dane mogą być z niej zwracane.
I teraz jeżeli chodzi o function overload,
jest kilka zasad których należy się definitywnie trzymać.
Przede wszystkim nazwy wszystkich
przeciążeń oraz samej funkcji muszą być dokładnie takie same.
Następnie parametry oraz ich liczba przy
każdym przeciążenia muszą się od siebie różnić.
W tym konkretnym przypadku liczba akurat
się nie różni ale różni się ich typ więc nie ma tutaj z tym problemu.
W momencie jednak gdybyśmy z jakiegoś powodu popełnili błąd no to zadziała tylko
to przeciążenie które zostało zadeklarowane jako pierwsze.
Następnie kolejną ważną zasadą jest to że
implementacja funkcji musi być zgodna z każdym zdefiniowanym przeciążeniem.
Inaczej mówiąc musi obsługiwać każdy
przypadek który zdefiniowaliśmy w przeciążenia.
W momencie gdybyśmy nie mieli tutaj np. ifa
albo zwracali jakieś inne dane skutkowałoby to wystąpieniem błędu
i ostatnią zasadą jeżeli chodzi o przeciążenia jest to, że układamy je w
kolejności od najbardziej konkretnego do najbardziej ogólnego. Czyli jeżeli
zadeklarowali byśmy tutaj przeciążenie przyjmujące typ any
no to za każdym razem to właśnie ono zostałoby wykorzystywane
i zobacz że dzieje się nawet tak pomimo tego że tutaj mamy zadeklarowane
przeciążenie bardziej precyzyjne ale już w momencie gdy zmienimy kolejność.
Ten pierwszy przypadek zostanie obsłużonych według pierwszego przeciążenia
a ten drugi według tego drugiego bardziej ogólnego.
Ostatecznie pamiętaj też o tym że
przeciążenia mogą dotyczyć różnej liczby argumentów.
Zobaczmy to jednak na przykładzie. Mamy
tutaj funkcję której działanie będzie się różniło w zależności od tego czy
przekażemy do niej jeden argument czy też dwa.
W momencie gdy ten drugi argument zostanie
przekazany podzielimy pierwszy argument przez niego. W sytuacji jednak gdy ten
argument nie będzie obecny podzielimy naszą liczbę przez dwa.
No i teraz jesteśmy w sytuacji w której
wykonanie tej funkcji nie zawsze zakończy się poprawnie.
Ten błąd akurat wynika z tego że musimy
ten parametr oznaczyć jako opcjonalny dodając tutaj znak zapytania.
Ale nawet pomimo tego TypeScript nadal nie orientuję się dokładnie co się dzieje z
przekazanymi danymi oraz jakie dane są zwracane kiedy.
Dlatego też najlepiej będzie jeżeli
zadeklaruje tutaj odpowiednie przeciążenia. Trzymając się teraz wszystkich zasad o
których wspomniałem przed chwilą definiuje pierwsze przeciążenie które,
obsługuje sytuację w której mamy do czynienia
wyłącznie z jednym argumentem. Czyli jest to przeciążenie bardziej precyzyjne od
tego w którym mamy do czynienia z dwoma argumentami. Ze względu na to że ten drugi
parametr oznaczony jest jako opcjonalny, i nie mamy tutaj żadnej do myśli wartości.
Wszystko jest w porządku. No i dodatkowo
TypeScript odpowiednio odróżnia poszczególne przypadki.
Mam nadzieję że to wszystko jest dla
Ciebie jasne i na ten moment to wszystko słyszymy się za chwilę w kolejnej lekcji.
upperAnything · 2 min
function upperAnything(data: string | string[]): string | string[] {
if (typeof data === "string") {
return data.toUpperCase();
}
return data.map(item => item.toUpperCase());
}
upperAnything(['eduweb', 'overment']) /*?*/
upperAnything('Adam') /*?*/