Wybierz region
pl
  • PL
  • EN
Wydrukuj

Zaawansowane typy w TypeScript

Pomimo iż wiele osób wciąż kojarzy JaveScript z animacjami na stronach internetowych, język ten wykorzystywany jest również po stronie serwera, w aplikacjach webowych, mobilnych czy desktopowych.

Szybki rozwój oraz coraz szersze spektrum zastosowań spowodowało zapotrzebowanie na dodatkowe możliwości, którymi dysponują inne języki programowania. Z tego powodu Microsoft wprowadził na rynek TypeScript stanowiący pewien nadzbiór JS, umożliwiający między innymi statyczne typowanie zmiennych, programowanie obiektowe czy typy generyczne.

Osoby mające doświadczenie w JS zdają sobie sprawę jakie konsekwencje może nieść za sobą brak typowania zmiennych. Przykładowo podanie jako argument zmiennej typu string zamiast number w prostym działaniu

spowoduje konwersję wartości liczbowych do ciągu znaków i ich konkatenację (otrzymamy złączone ze sobą dwa stringi). Metoda addNumbers może przyjąć argumenty jakiegokolwiek typu, JS nie „domyśli się”, że chcemy dodać do siebie dwie liczby.

W JS czeka na nas sporo, często zaskakujących pułapek. Dużym minusem jest brak możliwości wyłapania błędu przy kompilacji. Niestety wszystko poprawnie się skompiluje, ponieważ argumenty a i b w naszej funkcji addNumbers nie mają podanego typu. Właśnie tu z pomocą wkracza TypeScript, w którym jeżeli zatypujemy sobie a i b, a następnie podamy inny typ otrzymamy błąd już przy kompilacji.

Argument of type 'string' is not assignable to parameter of type 'number'.

Jeżeli cały kod aplikacji jest napisany w TypeScript to problem z typami teoretycznie nie powinien mieć miejsca (pod warunkiem, że nie używamy namiętnie typu any, co osobiście szczerze odradzam).

W TypeScript mamy oczywiście możliwość tworzenia własnych typów, nie musimy się ograniczać do tych podstawowych. Często w tym celu tworzymy interfejsy, dzięki którym możemy definiować strukturę obiektu. Aby usprawnić sobie pracę oraz wyeliminować w pewnym stopniu kopiowanie kodu warto poznać Utility Types. TypeScript zawiera kilkanaście specjalnych typów dzięki którym możemy tworzyć nowe typy z już istniejących.

Zdefiniujmy przykładowy interfejs Patient, na podstawie którego utworzymy wiele innych nowych typów.

1. Pick<Type, Keys> - tworzy nowy typ z wyłącznie wybranymi polami Keys.

 Wyobraźmy sobie sytuację, w której chcemy zaktualizować dane adresowe pacjenta. Potrzebujemy więc stworzyć obiekt jedynie z polem address oraz id. Pozostałe pola - firstName i lastName pozostają bez zmian. Gdyby nowo stworzony obiekt był typu Patient, w którym wszystkie pola są wymagane musielibyśmy nadmiarowo podać firstName oraz lastName. Szczególnie problematyczne okazałoby się to w przypadku obiektu z wieloma właściwościami – wtedy najprawdopodobniej stworzony by został nowy typ na potrzeby tego przypadku. Nie jest to jednak konieczne, możemy skorzystać z istniejącego już typuStworzony nowy typ posiada więc jedynie pola id i address. Oczywiście w tym przypadku, jak i w kolejnych poniżej możemy od razu zatypować sobie obiekt, nie ma potrzeby tworzenia aliasu typu.

 

W przypadku nie podania wymaganego id lub address od razu otrzymamy błąd.

Property 'address' is missing in type '{ id: number; }' but required in type 'Pick<Patient, "address" | "id">'

2. Partial<Type> - zwraca typ Type z ustawionymi wszystkimi polami jako opcjonalne.

Całkiem często zdarza się, że po prostu nie ma potrzeby podania wszystkich właściwości. Jak wcześniej potrzebowaliśmy jedynie pola id oraz address. Prostszym sposobem niż Pick jest ustawienie wszystkich pól jako opcjonalne. W przypadku naszego interfejsu Patient utworzony nowy typ PartialPatient nie ma ani jednego wymaganego pola. W takiej sytuacji trzeba jednak pamiętać aby podać wszystkie pola, których na przykład oczekuje serwer. Nie zaktualizujemy już danych adresowych pacjenta jeżeli zapomnimy o podaniu jego id.

3. Omit<Type, Keys> - działa w sposób odwrotny do Pick, czyli usuwa wybrane pola Keys.

W takim przypadku nasz nowy typ PatientWithoutAddress posiada id, firstName oraz lastName.

4. Required<Type> - odwrotność wspomnianego wcześniej Partial – ustawia wszystkie pola jako wymagane.

Bardzo przydatny typ. W kombinacji z omawianym wcześniej Pick możemy stworzyć typ z jednym wymaganym polem id.

5. Readonly<Type> - do każdej właściwości dodaje readonly.

Próba przypisania wartości do któregoś z pól skutkować będzie wystąpieniem błędu.

Cannot assign to 'id' because it is a read-only property.

Ponadto w TypeScript istnieją również dodatkowe dwa typy - Intersection Typ  oraz Union Type dzięki którym również możemy stworzyć nowy typ. Intersection Type czyli sposób na łączenie kilku typów w jeden. Używamy w tym celu znaku & podając które typy nas interesują. Powstały w ten sposób typ posiada właściwości kilku typów na raz. Union Type z kolei pozwala na opisanie typu, który jest jednym z kilku podanych. Typy oddzielamy |. Opisany poniżej nowy typ IntersectionType posiada wszystkie właściwości z Patient i User, natomiast typ Person jest typu Patient lub User. 

 

Na koniec Utility Types w wersji generycznej. Dobrym podejściem jest wytwarzanie kodu, który możemy użyć w wielu miejscach w naszej aplikacji. Błędną i niewydajną praktyką w programowaniu jest kopiowanie i wklejanie tych samych fragmentów w wielu miejscach. Spróbujmy połączyć wszystkie sposoby wcześniej opisane tworząc nowy typ, z którego będziemy mogli skorzystać w wielu różnych przypadkach.

Wykorzystujemy tu Partial, Omit, Required oraz Intersection Type jednak już nie jedynie dla typu Patient jak wcześniej, ale dla jakiekolwiek zadanego. Co właściwie powstanie z takiej kombinacji?

W pierwszej części Partial<Omit<T, K>> usuwa właściwość K z T i pozostałe pola ustawia jako opcjonalne.

W drugiej części Required<Pick<T, K>> sprawia, że zadana właściwość K ustawiana jest jako wymagana.

Połączenie obu skutkuje stworzeniem typu z jednym wymaganym K i wszystkimi pozostałymi polami opcjonalnymi. Możemy tu użyć stworzonego wcześniej Patienta (ale może to być tak naprawdę jakikolwiek typ) i uzyskamy poniższy rezultat

 

Oczywiście to nie wszystkie z dostępnych typów w TypeScript. Z ciekawszych wymienić mogę jeszcze Record<K,T> czy NonNullable lecz do dalszej lektury polecam oficjalną dokumentację. Znajomość możliwości jakimi dysponujemy w TypeScript pozwala nam wytwarzać lepszy, czytelniejszy kod, który łatwiej jest też utrzymać.


Martyna Podsiadły

Programistka z Gliwic. Interesuję się nowymi technologiami, uwielbiam uczyć się nowych rzeczy i nie boję się ambitnych wyzwań. Wolny czas lubię spędzać z moim psem Całką, znajomymi i dobrym jedzeniem.


Wydrukuj