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+
Typy generyczne to jeden z tych tematów, który daje nam niesamowite możliwości
głównie ze względu na elastyczność jaką oferuje.
Inaczej mówiąc są to typy dynamiczne które
możemy wykorzystywać zarówno w przypadku zmiennych, funkcji, interfejsów oraz klas.
Zobaczmy więc teraz jak to wygląda w praktyce.
Przede wszystkim muszę ci powiedzieć że z
typami generycznymi w pewnym sensie mieliśmy już do czynienia.
Konkretnie chodzi o to że w momencie gdy dodajemy typ do tablicy możemy to zrobić w
taki sposób albo alternatywnie skorzystać z zapisu generycznego.
W takiej sytuacji jak widzisz typ przekazaliśmy tutaj w nawiasie ostrym.
I właśnie dokładnie taka notacja wskazuje na typ generyczny.
Oczywiście tutaj nie ma żadnego
dynamicznego typu, ponieważ bezpośrednio wskazali na typ number.
A jak powiedziałem na początku w typach
generycznych chodzi przede wszystkim o dynamikę którą oferują.
Zatem spójrzmy teraz jeszcze na jeden przykład.
Mamy tutaj funkcję która przyjmuje jako
parametr dane typu number a następnie te dane zostają do nas zwrócone.
W tym momencie TypeScript doskonale już wie że ta
funkcja przyjmuje jako parametr number bo zresztą sami to określiliśmy.
A on sam był w stanie określić że zwracać też będziemy typ number.
I znowu nie ma tutaj żadnej dynamiki
ponieważ bezpośrednio wskazywaliśmy typ więc ta funkcja będzie działać tak długo
jak przekażemy do niej liczbę ale już w momencie gdy trafi do niej jakiś
inny typ no to TypeScript informuje nas o błędzie.
To akurat nie jest dla nas czymś nowym.
Teraz pytanie co możemy zrobić w momencie
gdybyśmy chcieli mieć taką samą funkcję tylko dla typu string.
No pierwsze co przychodzi do głowy to moglibyśmy skopiować tę funkcję.
No i teraz moglibyśmy ją wykorzystać.
W sytuacji gdybyśmy chcieli teraz taką samą funkcję ale dla typu array.
No to znowu musielibyśmy ją skopiować.
Przy czym w tym momencie byłem zmuszony do tego aby wykorzystać tutaj typ any.
Oczywiście alternatywnie mógłbym tworzyć
tutaj funkcje dla tablicy liczb, tablicy stringów itd.
Natomiast nie do końca ma to sens.
Wynika to z faktu że do dyspozycji mamy typy generyczne.
I to akurat mogę pokazać Ci na takim przykładzie.
Jak widzisz tworzę tutaj nowy alias typu do którego przekazuje literę T.
Reprezentuje ona przekazany do niego typ.
Zatem pod tą literą może znaleźć się zarówno typ number, string albo array.
Następnie ten przekazany typ przypisuję jako wartość.
W związku z tym jeżeli teraz utworzy nowy
alias a następnie wykorzystam tą fabrykę typów to zwróć uwagę że po przejechaniu
na textType mamy tutaj string, czyli dokładnie ten string został
przekazany w tym miejscu a następnie zwrócony tutaj.
Dzięki temu w tym miejscu otrzymaliśmy
nowy alias, który przyjmuje wartości typu string.
Zatem teraz nic nie stoi na przeszkodzie
abyśmy utworzyli zmienną typu textType do której możemy przypisać string.
No ale w tym momencie możesz zapytać, po co w ogóle robimy coś takiego.
Skoro moglibyśmy utworzyć nowe typy analogicznie do tego co mamy tutaj.
No faktycznie w tym momencie jest to
zasadne pytanie ale zobaczmy teraz na kolejny przykład.
Mam tutaj na myśli wykorzystanie typów generycznych w przypadku funkcji
analogicznie do wcześniejszych przykładów utworzymy tutaj funkcję, która będzie
przyjmować jakąś wartość a następnie tę wartość zwracać.
W tym momencie wartości, które ta funkcja przyjmuje oraz zwraca są ustawiona na any.
Jest to jak najbardziej zrozumiałe,
ponieważ nie wskazaliśmy tutaj żadnego typu.
Mało tego nie chcemy tego robić w sposób dosłowny,
dlatego wykorzystamy tutaj typ generyczny i podobnie jak w poprzednich
przykładach zapisujemy go w nawiasie ostrym i przekazujemy lliterkę T. Pamiętamy
teraz że pod tą literą będzie znajdował się każdy przekazany do tej funkcji typ.
W związku z tym ten typ musimy przypisać do parametru funkcji oraz do zwracanej
wartości. Ostatecznie tej utraconej wartości moglibyśmy nie sygnalizować,
ponieważ również bez niej TypeScript byłby w stanie się tego domyślić.
W każdym razie zostawmy teraz tak jak jest.
Mamy tutaj co prawda jeden mały błąd i wynika on z faktu że zmienna value którą
zdefiniowaliśmy tutaj została już utworzona w bieżącym zakresie.
Teraz wszystko jest tutaj w porządku.
W tej chwili mamy już wszystko do tego abyśmy mogli wykorzystać tę funkcję
dlatego utworze teraz nową zmienną którą automatycznie TypeScript
ustawił jako string literal type, przyjmujący jako wartość moje imię
a następnie tę wartość możemy wykorzystać w przypadku naszej funkcji.
Zwróć uwagę że po najechaniu na
wywołanie tej funkcji TypeScript automatycznie podstawił nam pod literkę T
string literal type, wyciągnięty z argumentu który przekazaliśmy do tej funkcji.
Alternatywnie jeżeli wskazali byśmy tutaj
typ string no to TypeScript również tutaj podstawi odpowiedni typ. Przy czym w
tym momencie Eslint mówi mi o tym że wcale nie muszę tego
robić, ponieważ od razu przypisuje tutaj wartość.
No i oczywiście ma tutaj rację.
W każdym razie to co teraz uzyskaliśmy jest niesamowite o tyle że jeżeli teraz
spróbuję wykonać jakąś metodę na zwróconej wartości, to zobacz że Visual Studio Code
od razu podpowiada mi metody które możemy wykonać na stringu.
Zatem mamy tutaj przykład korzyści
wynikającej z dodawania typów. W tym wszystkim najciekawsze jest jednak to, że
jeżeli przekazali byśmy do tej funkcji inną wartość a właściwie wartość innego typu no
to TypeScript również będzie w stanie tę wartość przekazać dalej
a my ponownie będziemy czerpać korzyści wynikające z tego faktu.
Najlepsze w tym wszystkim jest jednak to, że mamy tylko jedną funkcję do zarządzania
typami i uniknęliśmy wykorzystania typu any. W tej chwili chyba sam musisz przyznać, że
wykorzystanie typów generycznych jest niesamowicie proste. Po prostu trzeba nieco
przyzwyczaić się do tej nowej składnik która mało kojarzy się z samym JavaScriptem.
Oczywiście nie jest to też do końca tak że typy generyczne wykorzystujemy tylko w
takich błahych przykładach bo spójrzmy teraz na kolejny przykład w którym
utworzymy sobie fabrykę czyli funkcję zwracają są nowy obiekt.
Będzie ona przyjmować dwa argumenty a następnie zwracać je w postaci obiektu.
Dodam tylko że ostatecznie moglibyśmy tutaj skorzystać ze skróconej składni
ale zapisuje to wszystko celowo w ten sposób aby ci coś pokazać.
Teraz wywołując tą funkcję możemy przekazać do niej dwa argumenty.
Może to być liczba oraz tekst.
Wynika to z faktu że na ten moment w tym miejscu mamy tutaj any.
Oczywiście tego chcemy uniknąć, więc
wykorzystamy tutaj typy generyczne oraz interfacy.
Pierwszą rzeczą która rzuca się w oczy w
porównaniu do poprzedniego przykładu jest fakt że mamy tutaj dwa argumenty dlatego
też sygnalizują typy generyczne musimy to odpowiednio określić. Więc mamy tutaj
latarkę t oraz kolejną literę w alfabecie czyli U.
Te litery mogę odpowiednio przyporządkować poszczególnym parametrom.
Dzięki temu TypeScript jest w stanie już
dokładnie określić typ poszczególnych parametrów oraz typ zwracanej wartości.
W tym momencie zwracaną wartością jest tutaj obiekt.
Chciałbym teraz to zmienić i również
określić typ tego obiektu wykorzystując interfejsy.
Zatem tutaj definiuję nowy interfejs ale do niego również muszę przekazać typy
generyczne które będziemy przekazywać do niego z tej funkcji.
Dlatego przyporządkować je do nich kolejne litery alfabetu a następnie mogę je
przypisać do poszczególnych właściwości tego obiektu.
Mam nadzieję że na tym etapie jest to dla
Ciebie logiczne, ponieważ teraz po prostu wezmę ten interfejs i określę, że
funkcja getValues będzie zwracać wartość tego typu.
I tutaj w tej chwili mamy błąd ponieważ
TypeScript informuje nas o tym że musimy przekazać typy generyczne.
Dlatego to również robimy wykorzystując
typy przekazane do funkcji. I w tej chwili mamy już dokładną
informację o tym że funkcja getValues przyjmuje w tym konkretnym przypadku number
string i odpowiadają one pierwszemu oraz drugiemu argumentowali a następnie funkcja
ta zwraca dane typu określanego w interfejsie values.
I to jest kolejne nieco bardziej
zaawansowane przykład wykorzystania typów generycznych.
Dzięki temu zauważ, że tworząc nawet interfejsy jesteśmy w stanie skorzystać z tych
generycznych typów i przekazywać je z dowolnego miejsca np.
z funkcji.
Oczywiście nic nie stoi na przeszkodzie aby w tym miejscu znajdowała się np.
klasa.
Po prostu masz tutaj pełną dowolność jaką oferują typy generyczne.
Myślę że w tym momencie nic nie stoi na przeszkodzie abyśmy zobaczyli typy
generyczne w akcji w przypadku klas. Utworzę tutaj nową klasę, która będzie
zawierać jedną właściwość oraz dwie metody.
Pierwsza z nich będzie służyć do tego aby
dodawać elementy do naszej listy a druga żeby wyświetlać elementy tej listy.
W tej chwili jak widzisz mam tutaj typ any.
Ponieważ nie mam dokładnej informacji o tym jaki typ danych będzie przechowywany
na tej liście, bo w momencie gdy utworzę nową instancje tej listy a następnie
przekażę do niej jakieś wartości no to dopiero tutaj na tym etapie mam informację
o tym że faktycznie będzie to tekst, ale ze względu na to że mamy tutaj typ any.
Nic nie stoi na przeszkodzie abyśmy zapisali tutaj również liczbę.
I w tej sytuacji doprowadziliśmy do momentu w którym w tablicy znajdują się
dane różnego typu a TypeScript nie ma o nich zielonego pojęcia.
Dlatego musimy się tym zająć wykorzystując
właśnie typy generyczne. I typ generyczny przede wszystkim możemy
przekazać w tym miejscu i będzie on przekazany do instancji tej klasy.
Dlatego też muszę to zasygnalizować w tym miejscu.
No i teraz tą literę T mogę zastosować tutaj, tutaj oraz w tym miejscu.
Dzięki temu tak utworzona instancja naszej klasy ma już informację o tym, że
może przyjmować wyłącznie tekst ale liczb już nie.
Dlatego TypeScript w tym momencie jest w stanie poinformować nas o błędzie.
I swoją drogą w tym miejscu również pewnie przyznasz mi rację że było to niesamowicie
proste po prostu musiałem określić jaki typ przekazuje do klasy a następnie
zasygnalizować ten typ generyczny w momencie gdy tę klasę definiuje oraz
wykorzystać go wewnątrz tej klasy wszędzie gdzie jest to potrzebne.
No i dzięki temu nasza klasa jest o tyle
generyczna że możemy odtworzyć na jej podstawie różnego rodzaju instancje np.
takie które będą przyjmować chociażby liczbę.
Wystarczy że sygnalizuje odpowiedni typ.
No i ponownie wszystko gra a my nie musimy powtarzać niepotrzebnie zbędnego kodu.
Także jak widzisz typy generyczne odgrywają ogromną rolę w kontekście
dodawania typów bez konieczności powtarzania definicji np.
klas interfejsów czy aliasów
typów.
Dzięki temu zyskujemy tą wspomnianą
elastyczność oraz w momencie gdy opanuje już ten mechanizm i będziemy czuć się z
nim komfortowo zyskujemy również ogromny komfort działania.
Na ten moment jeżeli chodzi o wstęp do
typów generycznych to już tyle a ja zapraszam Cię do kolejnej lekcji.
Array generic · 1 min
// Array generic
const numbers: number[] = [];
const arr: Array<number> = [];Generic types · 3 min
// Generic types
const getNumber = (x: number) => x;
const getText = (x: string) => x;
const getArray = (x: Array<any>) => x;
// const value = getText('5'); /*?*/
type TypeFactory<T> = T;
type textType = TypeFactory<string>; /*?*/
const text: textType = 'Message';Generic functions · 6 min
// Generic functions
function getValue<T>(value: T): T {
return value;
}
const username = 'Adam';
getValue(username);
getValue(5);Generic interfaces · 9 min
// Generic interfaces
interface Values<V, W> {
val1: V,
val2: W
}
function getValues<T, U>(arg1: T, arg2: U): Values<T, U> {
return {
val1: arg1,
val2: arg2
};
}
const values = getValues(2, 'text');Generic classes · 11 min
// Generic classes
class List<T> {
private list: T[] = [];
addItem(item: T): void {
this.list.push(item);
}
getList(): T[] {
return this.list
}
}
const list = new List<string>();
const ids = new List<number>();
list.addItem('Test');
ids.addItem(1234);
list.getList(); /*?*/