Składnia języka
W poprzednim artykule poznaliśmy podstawowe założenia stojące za TypeScriptem, ale nie napisaliśmy jeszcze ani jednej linii kodu w tym języku. Najwyższy czas przejść od teorii do praktyki.
Musisz wiedzieć, że statyczny typ można dodać do niemal wszystkich elementów języka, m.in. do zmiennych, parametrów funkcji, wartości zwracanej z funkcji, czy też do pola klasy. Na początku skupimy się jednak na zmiennych. Jest to podstawowy element każdego języka programowania, dlatego warto zacząć właśnie od nich.
Na wstępie warto jeszcze zaznaczyć, że nie poruszę w tym artykule wszystkich możliwych aspektów związanych ze składnią stosowaną w TS (zdecydowanie nie jest to możliwe w jednym, krótkim tekście), skupiam się na podstawach. Wszelkie bardziej zaawansowane zagadnienia będą omówione w kolejnych artykułach.
Statycznie typowane zmienne
W dużej mierze (ale nie tylko, są również inne sposoby) wykorzystywany jest zapis z użyciem dwukropka, który występuje po nazwie, następnie musimy wskazać jakiego typu będzie dana zmienna.
W praktyce taki zapis wygląda tak:
Do zmiennej o nazwie num
przypisaliśmy statyczny typ number
, a następnie przypisaliśmy jej wartość 15
. Oczywiście wartość number
nie jest przypadkowa. Twórcy TSa zaimplementowali kilka wbudowanych w język typów, a number
, to właśnie jeden z nich. Resztę omówimy za chwilę.
Zanim przejdziemy dalej poruszmy jeszcze jedną kwestię - początkujący użytkownicy TypeScriptu często popełniają błąd polegający na zapisaniu typu z wielkiej litery. Niestety nie pomaga tu również sam TypeScript, ponieważ typ String
jest poprawnym zapisem i odpowiada za obiekt String
znany nam z JS. Warto zapamiętać, że typy podstawowe zawsze zapisujemy z małej litery. Wspomina o tym również dokumentacja.
Zastanawiasz się być może, co się stanie, jeżeli do zmiennej num
przypiszemy teraz wartość, która nie jest liczbą, np. true
. W takim przypadku otrzymamy błąd, mówiący nam o tym, że nie możemy przypisać wartości typu boolean
do zmiennej o typie number
.
Typy podstawowe
Wspomnieliśmy wcześniej o dwóch typach - number
oraz boolean
. Jak zapewne się domyślasz jest ich trochę więcej. Jeżeli jednak znasz już trochę JavaScript, to w dużej mierze będą one dla Ciebie znajome.
number - reprezentuje wszystkie wartości numeryczne. Nie ma tu podziału na
float
,int
, itp. jak to ma miejsce w innych statycznie typowanych językach. Wyjątkiem są tutaj wartości reprezentowane przezBigInt
- jeżeli używamy funkcjiBigInt()
, lub dodaliśmy literęn
na końcu wartości liczbowej (co również sprawia, że tworzymyBigInt
), to powinniśmy otypować zmienną w następujący sposóbstring - do tego typu przypisać można wszystkie wartości tekstowe, włącznie z
template string
boolean - reprezentuje tylko dwie wartości:
true
orazfalse
literal - w przeciwieństwie do poprzednich trzech typów, które określały zbiór wartości, ten typ wskazuje na konkretne wartości tekstowe, liczbowe lub boolowskie. Zamiast wskazywać, że do zmiennej przypisać można typ
number
, możemy "powiedzieć" TypeScriptowi, że może się tam znaleźć tylko konkretna liczba.Na tym etapie może się to wydawać zupełnie nieprzydatne, jednak gdy tylko poznasz unie, typy generyczne i inne bardziej zaawansowane elementy języka, ten typ nabierze znacznie więcej sensu. Warto jednak wspomnieć o nim już teraz, ponieważ jego znajomość przyda się podczas wyjaśniania mechanizmu inferencji typów.
null - bardzo specyficzny typ przeznaczony jedynie dla wartości
null
undefined - podobnie jak wyżej. Przypisać można tu jedynie
undefined
. Zauważ, że w TypeScriptnull
iundefined
nie są wymienne, a więc do zmiennej otypowanej jakonull
, nie można przypisaćundefined
.any - jest niemal równoznaczny z brakiem typu. Przypisać można tu dosłownie każdą wartość i choć ma swoje zastosowanie, to powinieneś zdecydowanie go unikać, kiedy to tylko możliwe. Użycie go "wyłączą" całą ochronę jaką daje nam TypeScript i cofa nas do systemu znanego z JavaScript.
Nie są to wszystkie typy proste, jednak z tych będziesz korzystać codziennie i w zupełności wystarczą do poznania podstaw.
Być może już teraz zauważasz, że trzeba napisać sporo dodatkowego kodu w miejscach, gdzie nie jest to tak naprawdę potrzebne. Skoro stworzyliśmy zmienną num
z użyciem słowa const
, to i tak jej wartość nie ulegnie zmianie (przynajmniej w przypadku typów prostych), po co więc dodawać do niej jeszcze typ?
Twórcy języka też to zauważyli, dlatego wprowadzili do niego mechanizm inferencji.
Inferencja typów
To mechanizm, który pozwala TypeScriptowi "domyślić się" jaki typ powinien przypisać do danej zmiennej (mimo, że inferencja działa też dla innych wartości, to skupimy się tu na zmiennych). Oznacza to, że nie musimy zawsze jawnie deklarować typu. W większości prostych przypadków, a czasem nawet w tych bardziej zaawansowanych TypeScript potrafi poprawnie zainferować typ.
Jeżeli korzystasz z edytora kodu, który wspiera TS (np. Visual Studio Code), to wystarczy, że najedziesz myszką na nazwę zmiennej, żeby zobaczyć jaki typ został do niej przypisany.
W przykładach poniżej przedstawiam działanie inferencji dla dwóch typów prostych.
Jak widzisz w przypadku zmiennych const
wywnioskowany przez TS typ jest literałem - wskazuje na konkretne wartości. Dla zmiennej str
jest to "some string"
, a dla zmiennej num
15
.
Jednak w przypadku zmiennych str2
i num2
, utworzonych za pomocą słowa let
sprawa ma się nieco inaczej. Tutaj TypeScript nadał znacznie bardziej ogólne typy - string
oraz number
. Dlaczego tak się dzieje?
Podczas inferencji typu, TS stara się określić typ tak dokładnie, jak to tylko możliwe, nie ograniczając przy tym programisty. W przypadku zmiennych const i przypisanych do nich typów prostych jest to bardzo proste zadanie, ponieważ wartości nigdy nie ulegną zmianie. W związku z tym zainferować można literał, zamiast bardziej ogólnego typu.
Dla zmiennej let
, sprawa ma się inaczej, ponieważ wartość do niej przypisana może bez problemu zostać nadpisana w trakcie działania programu. Gdyby podczas inferencji, został tu przypisany literał, to w czasie ponownego przypisania otrzymalibyśmy błąd (chyba, że nadpiszemy tą samą wartością), a nasza praca z TSem byłaby bardzo frustrująca.
Jest jeszcze jedna kwestia związana z deklaracją zmiennych - w przypadku użycia słowa let
, nie trzeba od razu przypisywać wartości, a więc można zadeklarować zmienną bez jej inicjalizacji.
Z dostępnych danych TS nie może wywnioskować jaką wartość programista chce przypisać do zmiennej w trakcie działania programu. Stosuję więc tą samą zasadę, co wcześniej - nie chce ograniczać programisty. Jedyny możliwym rozwiązaniem jest tutaj typ any
.
Jeżeli nie planujesz od razu zainicjalizować zmiennej, to koniecznie jawnie nadaj jej typ, inaczej automatycznie zostanie przypisany any
, co w konsekwencji prowadzi do utraty statycznego typowania i wszystkich plusów z tym związanych.
Podsumowując: jeżeli nie przypiszemy typu jawnie, to TypeScript postara się go wywnioskować na podstawie dostępnych danych i zrobi to tak dokładnie, jak to tylko możliwe. Jednocześnie postara się nie narzucać żadnych ograniczeń programiście. Są jednak sytuacje, w których inferencja nie jest w stanie sobie poradzić, dlatego za każdym razem warto sprawdzić, jaki typ został przypisany.
Dobre praktyki
Na koniec dwa zdania na temat tego czy należy jawnie deklarować typ zmiennej, czy też nie.
Odpowiedź na to pytanie nie jest jednak tak prosta. W przypadku zmiennych const
, co do których nie ma wątpliwości, że typ został poprawnie zainferowany nie należy dodawać jawnej deklaracji typu, ponieważ nie niesie to za sobą żadnych benefitów, a jedynie utrudnia odczytanie kodu aplikacji.
Oczywiście od tej reguły są wyjątki. W przypadku bardziej skomplikowanych typów, np. obiektów lub tablic, istnieje obawa, że podczas tworzenia programista może się pomylić (np. zrobić literówkę w kluczu obiektu), a co za tym idzie, kod nie będzie działać poprawnie. Nawet jeśli nie Ty popełnisz błąd podczas inicjalizacji zmiennej, to może zrobić to ktoś inny, gdy będzie przeprowadzać refactor, lub po prostu wprowadzać zmiany. W sytuacji, gdy mamy do czynienia ze skomplikowanym typem, warto jawnie go zadeklarować, nawet jeżeli inferencja działa jak należy.
Ta zasada przyda Ci się na późniejszym etapie nauki, ale już teraz dobrze wiedzieć, że jawna deklaracja nie zawsze jest zła, a inferencji nie można ufać bezgranicznie.
Dla zmiennych zadeklarowane z użyciem let
sytuacja wygląda podobnie. Dodatkowo, trzeba pamiętać o jawnej deklaracji typu, dla zmiennych, które nie zostały od razu zainicjalizowane.
W kolejnych artykułach, kiedy już poznasz zaawansowane konstrukcje TypeScriptu, postaram się wdrożyć w życie powyższą zasadę. Trudno jest określić sztywną granicę kiedy należy dodać jawną deklarację typu, często zależy to od osobistych preferencji programisty. Wraz ze wzrostem doświadczenia łatwiej będzie Ci rozpoznać takie sytuacje.
Podczas swojej kariery natknąłem się także na drugą szkołę, o której warto wiedzieć.
Był to zespół, który jawnie deklarował wszystkie zmienne i miał kilka argumentów na poparcie takiego podejścia:
Od razu widać jakiego typu jest dana zmienna, nie trzeba nawet najechać na nią myszką
Podczas zmian w kodzie, możemy uniknąć błędów związanych z niezamierzoną zmianą przypisanej wartości
Ostatnim argumentem był fakt, że większość osób w tym zespole programowała wcześniej w statycznych językach, gdzie deklaracja typu jest (lub wtedy była) zawsze wymagana
Jak widzisz dobre praktyki to kwestia dyskusyjna i znajdziesz na ich temat w internecie mnóstwo opinii. Ja postaram się przekazać te, które sam stosuję i które osobiście uważam, za najlepsze, nie oznacza to jednak, że jest to jedyna słuszna droga.
Podsumowanie
Poznałeś właśnie podstawową składnię języka oraz kilka, najczęściej wykorzystywanych typów. Na tym etapie powinieneś rozumieć również jak działa inferencja (będziemy wracać do tego tematu w kolejnych artykułach, wtedy z pewnością bardziej się z nią oswoisz). Kolejny krok to wykorzystanie zdobytej wiedzy w praktyce. Zachęcam Cię, żebyś również samodzielnie eksperymentował z tym, czego nauczyłeś się do tej pory. Najlepszym miejscem będzie oficjalny playground.