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+
Kontynuując temat typów generycznych musimy porozmawiać o tzw.
ograniczeniach generycznych.
Chodzi o to że w niektórych sytuacjach
chcemy aby typy generyczne nie dopasowywały się automatycznie do każdego typu tylko do
tych przez nas wybranych bądź takich które posiadają konkretne właściwości.
Weźmy na przykład funkcję która będzie
zwracać nam długość tablicy bądź ciągu znaków.
Parametrem jest tutaj wspomniany ciąg znaków bądź tablica a zwracaną wartością
liczba. W tym momencie TypeScript oczekuje od nas tego abyśmy określili typ danych więc
możemy powiedzieć że będzie to string lub tablica.
No i teraz tę funkcję możemy wykonać zarówno na tablicy jak i ciągu znaków.
Wszystko działa poprawnie.
No może z tą różnicą że mamy tutaj typ any.
Moglibyśmy to doprecyzować i wstawić np.
number lub jeszcze lepiej skorzystać z typu
generycznego. W takiej sytuacji zwróć uwagę, że w tym miejscu nawet nie musiałem
podawać tutaj typu number ponieważ TypeScript samodzielnie był w stanie go określić.
No i teoretycznie wszystko tutaj działa
poprawnie, ale jak doskonale wiesz właściwość length mogą posiadać różne inne
obiekty nawet takie zdefiniowane przez nas.
W takiej sytuacji nasza funkcja nie do końca zadziała.
I właśnie rozwiązaniem tego problemu są ograniczenia generyczne.
Możemy tutaj po prostu ustawić typ
przekazanego parametru na typ generyczny ale jednocześnie wyżej stworzymy sobie
tutaj interfejs w którym stworzymy typ zawierający właściwość length.
Dzięki temu możemy wykorzystać go w
ograniczeniu generycznym wykorzystują słowo kluczowe extendeds.
Taki zapis oznacza, że przekazany przez nas typ musi być zgodny z interfejsem length.
A ten interfejs dba tylko o to aby przekazane dane posiadały właściwość length.
Dlatego też w tej sytuacji wszystko działa poprawnie zarówno z tablicami tekstem oraz
dowolnym i innymi obiektami które będą tylko posiadały tę właściwość.
Dodatkowo jeżeli pamiętasz to z lekcji o
interfejsach. Możemy korzystać z wielu różnych interfejsów.
Różnicą jest jednak to że nie podajemy
kolejnego interfejsu po przecinku tylko wykorzystujemy znak &.
W tej sytuacji wystarczy tylko, że zdefiniuję ten dodatkowy interface.
No i wszystko będzie w porządku. No może
poza tym, że nasza funkcja przestała tutaj działać dla przekazanego ciągu znaków
a wynika to z faktu że string w przeciwieństwie do tablicy
nie posiada wbudowanej metody filter. Tak, że wszystko jest w porządku a my
uzyskaliśmy bardzo generyczną funkcję, która przyjmuje dane posiadające wyłącznie
te wartości, które określiliśmy w interfejsach.
Tego typu wykorzystanie ograniczeń
generycznych może być przydatne chociażby w przypadku komponentów w React.js
które przyjmują poszczególne propsy bądź state.
Dzięki temu jesteśmy w stanie sprawdzić czy oczekiwane przez nas procesy
faktycznie zostały przekazane, albo czy mają odpowiedni kształt czyli np.
pod napisem length znajduje się faktycznie liczba. Podkreślę tutaj tylko jeden mały
fakt mianowicie w tym przypadku skorzystaliśmy z ograniczeń generycznych w
przypadku funkcji ale tak jak powiedziałem w poprzedniej lekcji genetyki możemy
stosować również z interfejsami, klasami itd.
Zresztą myślę że mogę pokazać Ci to na jednym przykładzie.
Mamy tutaj klasę song która posiada dwie właściwości tytuł oraz rok wydania.
Możemy też napisać funkcję która będzie przyjmować ten utwór a następnie zwracać
ciąg znaków zawierający informacje o tym utworze.
I teraz wystarczy że otworzymy nowy obiekt
na podstawie tej klasy a następnie wywołany naszą funkcję.
I faktycznie wszystko działa poprawnie.
Ale znowu chcąc uczynić naszą funkcję bardziej generyczną chciałbym również aby
pracowała ona z obiektami posiadającymi właściwości title oraz year.
Ale jeżeli byłeś uważny przy okazji
poprzedniego kursu wspomniałem tam o tym że TypeScript nie będzie miał problemu jeżeli
przekażemy do funkcji getInfo która spodziewa się typu song.
Dowolny inny obiekt który kształtem pasuje do naszego utworu.
Jak widzisz wszystko działa poprawnie.
I tak TypeScript w żaden sposób nie informuje
nas o tym że cokolwiek tutaj nie gra. No bo faktycznie wszystko jest w porządku.
Powodem jest oczywiście sposób porównywania typów w TypeScripcie czyli tzw.
Duck-Typing.
TypeScript zakłada że skoro przekazany obiekt posiada te właściwości no to nie powinno
być problemu z tym aby tę funkcję tutaj wywołać.
Jeżeli jednak chcielibyśmy zastosować
tutaj typ generyczny aby wszystko wyglądało nieco bardziej poprawnie
moglibyśmy tutaj zastosować słowo kluczowe extendeds.
Na klasie song.
Dzięki temu jeżeli tutaj wstawię literki T no to znowu wszystko będzie w porządku
i zarówno w przypadku gdy przekażemy utwór TypeScript będzie wiedział że mówimy
tutaj o klasie song a z kolei gdy przekażemy tutaj dowolny
inny obiekt no to TypeScript również będzie o tym poinformowany.
Bo jeżeli teraz wrócimy do sytuacji którą mieliśmy przed chwilą czyli sytuacji w
której getInfo spodziewało się typu song to nawet pomimo tego że przekazaliśmy
tutaj obiekt który nie jest instancją klasy song no to dalej tutaj TypeScript jest
przekonany że jednak jest to utwór także raz jeszcze musisz zapamiętać tyle, że
wykorzystanie typów generycznych oraz ograniczeń generycznych może być związane
zarówno z interfejsami funkcjami oraz klasami.
Na sam koniec może pojawić Ci się pytanie
kiedy w ogóle korzystać z typów generycznych.
Na to pytanie odpowiadają dwa scenariusze.
Pierwszy z nich mówi o sytuacji, w której
funkcja interfejs lub klasa muszą współpracować z różnego rodzaju typami
danych których z jakiegoś powodu nie jesteś w stanie określić np.
unią bądź po prostu nie chcesz tego robić ze względu na to że tych typów jest tak
dużo. A drugim scenariuszem jest przypadek który widzieliśmy w poprzednim odcinku to
znaczy mamy tutaj klasę która wykorzystuje konkretny typ w wielu różnych miejscach.
Dzięki temu że zastosowaliśmy tutaj typ generyczny możemy wykorzystywać go z
powodzeniem w wielu różnych miejscach i nawet w sytuacji gdy ten typ będzie się
zmieniał na przestrzeni dalszego rozwoju aplikacji to dalej będzie to typ
generyczny, więc modyfikacja takiego kodu będzie zdecydowanie prostsza.
Na ten moment jeżeli chodzi o ograniczenia generyczne to już wszystko.
A ja zapraszam Cię do kolejnej lekcji.
howLong function · 2 min
function howLong<T>(data: T): number {
return data.length;
}
howLong([1, 2, 3]) /*?*/
howLong("Abc") /*?*/Length Filterable · 3 min
interface Length {
length: number
}
interface Filterable {
filter: Function
}Song class · 4 min
class Song {
constructor(public title: string, public year: number) {}
}
function getInfo<T extends Song>(song: T): string {
return `${song.title} @ ${song.year}`;
}
const song = new Song("We Will Rock You", 1977);
getInfo(song) /*?*/
const movie = { title: 'Gone In 60 Seconds', year: 2000 }
getInfo(movie) /*?*/