Ten artykuł jest częscią serii

Jeśli nie przeczytałeś jeszcze poprzednich artykułów, to zawsze możesz to nadrobić klikając w przycisk.

Miniatura artykułu

TypeScript - typy podstawowe i inferencja

11 minut

Skopiuj link

Data publikacji: 5/5/2023, 4:59:14

Ostatnia aktualizacja: 4/1/2024

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 przez BigInt - jeżeli używamy funkcji BigInt(), lub dodaliśmy literę n na końcu wartości liczbowej (co również sprawia, że tworzymy BigInt), to powinniśmy otypować zmienną w następujący sposób

  • string - do tego typu przypisać można wszystkie wartości tekstowe, włącznie z template string

  • boolean - reprezentuje tylko dwie wartości: true oraz false

  • 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 TypeScript null i undefined nie są wymienne, a więc do zmiennej otypowanej jako null, 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.

Avatar: Wojciech Rygorowicz

Software Engineer / Fullstack developer

Wojciech Rygorowicz

wojciech.rygorowicz@gmail.com

Podziel się na

Dodaj komentarz

Komentarze (0)

Brak komentarzy

Jeżeli zainteresował Cię ten artykuł koniecznie przeczytaj inne artykuły z tej serii