Miniatura artykułu

TypeScript - typy mapowane

11 minut

Skopiuj link

Data publikacji: 8/10/2023, 5:47:12

Ostatnia aktualizacja: 4/1/2024

Do czego służą?

Podczas pracy z TypeScriptem, dosyć często można spotkać się z następującą sytuacją - utworzyliśmy jakiś typ (np. unię, obiekt, tablice), a teraz potrzebujemy na jego podstawie stworzyć kolejny typ obiektowy, którego kluczami mogą być przykładowo, elementy unii.

Drugim przypadkiem z którym spotkasz się często, jest potrzeba utworzenia typu, na podstawie istniejącej wartości. Zdarzają się wartości, które są stałe i w związku z tym nie posiadają osobnego typu, ponieważ opieramy się na inferencji.

W obu sytuacjach z pomocą przyjdą typy mapowane (mapped types). Z ich pomocą możemy tworzyć nowe typy obiektowe, bazujące na istniejących typach lub wartościach. Posiadają również sporo możliwości modyfikacji i dostosowywania tworzonego właśnie typu, do potrzeb programisty.

Zanim przejdziemy do samych typów mapowanych, musimy poznać kilka słów kluczowych, które będą bardzo często używane w czasie pracy z nimi.

keyof

Z jego pomocą możemy odczytać wszystkie klucze z typu obiektowego i utworzyć z nich unię literałów (w przypadku, w którym obiekt zwiera więcej niż jeden klucz) lub literał (w przypadku, gdy obiekt ma tylko jeden klucz).

W utworzonym typie, literałem może być zarówno string, jak i number. To jaką wartość otrzymamy, zależy od tego, jakim typem były klucze, w użytym obiekcie.

Stworzyliśmy dwa typy: Person oraz Answer. W przypadku pierwszego z nich otrzymamy unię literałów string, ponieważ Person ma kilka kluczy, a każdy z nich jest stringiem. W drugim przypadku wynikiem jest unia literałów number.

typeof

Ten operator występuje w natywnym JavaScript, jednak jego działanie w TypeScript jest nieco inne. To, jak zostanie on potraktowany zależy od miejsca użycia. Jeżeli zostanie on użyty w miejscu, gdzie spodziewamy się typu, to zostanie potraktowany przez TS nieco inaczej.

Jego działanie sprowadza się do zamieniania wartości na typ. Jest bardzo przydatny, kiedy na podstawie jakiejś wartości chcemy utworzyć typ, a co za tym idzie, jest bardzo często wykorzystywany w typach mapowanych.

in

W działaniu przypomina pętlę for...in. Jego zastosowanie ogranicza się jedynie do typów mapowanych. Zapis wygląda również podobnie: K in keyof SomeObjectType. Mimo, że na początku nie będzie on oczywisty, to należy on nim myśleć jak o pętli dla typów - K to generyczny typ, pod który po kolei będzie podstawiony każdy typ z unii keyof SomeObjectType.

Pierwszy typ mapowany

Skoro wiemy już jakich operatorów będziemy używać i znamy ich działanie, to czas najwyższy, żeby to wszystko złożyć w całość i napisać pierwszy typ mapowany.

Przykład, którym się posłużymy będzie możliwie prosty, co pozwoli się skupić na składni, a nie na przesadniej skomplikowanej logice kodu.

Przypuśćmy, że utworzyliśmy typ przechowujący dostępne imiona - w naszym przypadku będzie to unia literałów stringa: "Natalia" | "Alicja" | "Karolina". Na jej podstawie chcemy utworzyć nowy typ, a konkretnie obiekt, którego kluczami będą imiona z utworzonej wcześniej unii, a wartością typ User (utworzymy go za chwilę).

Być może przyszło Ci już do głowy, że można tu także zastosować wbudowany typ generyczny Record i osiągnąć zamierzony efekt (jeżeli nie spotkałeś się z nim wcześniej, to opisałem go w tym artykule). Masz rację, ale tak jak wspomniałem pierwszy przykład ma być możliwie prosty w celu zrozumienia składni. Tak więc do dzieła!

To teraz po kolei, co dzieję się w powyższym przykładzie:

  1. Tworzymy typ Name przechowujący imiona

  2. Tworzymy typ User, który ma być odzwierciedleniem użytkownika w naszej aplikacji

  3. Za pomocą typu mapowanego tworzymy nowy typ obiektowy Users, którego struktura wygląda identycznie jak typu UsersWithoutMappedTypes. Oba te typy wyglądają dokładnie tak samo, jednak z użyciem typu mapowanego zrobiliśmy to znacznie szybciej, a dodatkowo, jeżeli Name ulegnie zmianie, to automatycznie będzie to odwzorowane w Users. W przypadku UsersWithoutMappedTypes zmiany musiałbym wprowadzić ręcznie.

To teraz teoria dotycząca zapisu związanego z typami mapowanymi:

  • Podobnie jak typ obiektowy, tak samo typ mapowany tworzymy za pomocą nawiasów klamrowych { ... }

  • Wewnątrz nawiasów kwadratowych [ ... ] umieszczamy pętlę, a po : typ, który zostanie przypisany do każdego klucza

  • Pętlę wygląda następująco: K in Keys. K to typ generyczny, dlatego jego nazwa jest dowolna, najczęściej jednak stosuje się K lub Key, natomiast keys to string, symbol lub number (lub unia tych typów)

Typ na podstawie wartości

Do tej pory nie użyliśmy jeszcze ani operatora keyof, ani typeof. Wprowadzimy je po kolei - zaczniemy od keyof, następnie typeof, a na samym końcu połączymy oba te operatory w jednym wyrażeniu.

Żeby nieco uprościć ten przykład posłużymy się utworzonym wcześniej typem Users (patrz przykład wyżej). Na jego podstawie utworzymy kolejny typ obiektowy, jednak zamiast przypisywać ponownie User jako typ wartości, użyjemy tym razem czegoś innego.

z pomocą operatora keyof odczytaliśmy klucze z typu Users, a następnie użyliśmy ich do utworzenia nowego typu obiektowego. Ten zapis można nieco skrócić, w przykładzie powyżej celowo rozpisałem go na osobne kroki, jednak częściej spotkasz się z takim zapisem:

Dzięki temu podejściu nie musimy tworzyć pośredniego typu, który będzie przechowywał odczytane klucze. Zamiast tego klucze odczytujemy dopiero w momencie, w którym tworzymy typ mapowany.

Czas dołożyć do tego wszystkiego operator typeof i utworzyć typ na podstawie wartości.

Podobnie jak poprzednio, tu również nie musimy tworzyć pośredniego typu UserKey. Możemy zastosować wyrażenie keyof typeof users bezpośrednio w typie mapowanym.

Modyfikatory

Podczas tworzenia typu mapowanego można również dodać, lub usunąć modyfikatory readonly oraz ? (opcjonalna wartość). Jak to zrobić?

Jeżeli chcemy usunąć istniejący modyfikator, to wystarczy dodać znak - przed danym modyfikatorem: -readonly, -?. W przypadku dodawania możemy również wstawić znak + przed modyfikatorem, jednak nie jest to konieczne.

Przekształcenia

Jest to dość zaawansowany temat, jednak w praktyce bardzo często wykorzystywany.

Przkeształcenia (być może to nie najlepsze tłumaczenie - po angielsku ten termin to remapping) pozwalają na modyfikację kluczy i dostosowanie ich do tworzonego właśnie typu.

Na początek coś stosunkowo prostego. Mamy utworzony obiekt, który jest niezmienny, zatem całość jest zapisana wielkimi literami w celu oznaczenia, że jest to wartość, która nigdy nie ulegnie zmianie w czasie działania programu. Na jego podstawie utworzymy typ, jednak nie chcemy żeby klucze były zapisane wielkimi literami. Z pomocą przychodzi remapping kluczy.

Nowością w tym zapisie jest wykorzystanie słowa kluczowego as w celu przekształcenia klucza. Składnia wygląda następująco: K in Keys as OtherType. Zapis ten oznacza, że chcemy zmienić typ K na typ, który występuje po słowie kluczowym as.

Zatem K zostanie zamienione na OtherType. Może się tam znaleźć dowolny typ, ale w praktyce najczęściej są to wariacje typu K. Jeżeli jednak jest taka potrzeba, to możesz równie dobrze wstawić w to miejsce unknown.

Zobaczmy jeszcze jeden przykład remappingu, tym razem nieco bardziej rozbudowany.

Jest to na tyle skomplikowany przykład, że warto rozbić go na części pierwsze:

  1. Z użyciem składni K in keyof User, tworzymy pętlę, zatem pod K zostanie podstawiony każdy klucz po kolei

  2. Z pomocą remappingu i słowa kluczowego as przekształcamy każdy z kluczy

  3. Wykorzystujemy do tego wbudowany w język generyczny typ Exclude, który usuwa z przekazanego typu (lub unii) wskazaną wartość - w naszym przypadku jest to id.

  4. Do Exclude zawsze przekazujemy generyczny typ K pod którym znajduje się każdy klucz po kolei. W momencie, gdy pod K znajduje się wartość "id", to transformacja wygląda tak:
    ... as Exclude<"id", "id">, a wynikiem tego działania jest typ never (nie można do niego przypisać żadnej wartości).

Jeżeli jakikolwiek z kluczy zostanie przekształcony na typ never, to zostanie on usunięty z nowo utworzonego obiektu. Zatem zapis ... as never usuwa dany klucz. Podobne zachowanie występuje w przypadku unii typów. Jeżeli jeden z jej elementów to never, to zostanie on po prostu usunięty.

Dzięki temu mechanizmowi możemy użyć Exclude do usuwania wybranych kluczy z typu.

Podsumowanie

Typy mapowane są dynamiczne - tworzone na podstawie innych typów. Mają wiele zastosowań i potrafią być bardzo rozbudowane, szczególnie kiedy zastosujemy wiele przekształceń (a na dodatek dodamy do tego przeróżne warunki z wykorzystaniem typów warunkowych), ale dzięki temu są również uniwersalne jeżeli chodzi o zastosowanie.

Na początku ich zastosowanie w praktyce może być trudne, a składnia nieintuicyjna, ale z czasem stanie się zupełnie naturalna (jak większość zagadnień związanych z programowaniem, kluczem jest wytrwałość i cierpliwość).

Ich zwięzła składnia pozwala na tworzenie bardzo rozbudowanych typów przy niewielkim nakładzie pracy. Jeżeli spotkasz się z sytuacją w której tworzysz typ obiektowy na podstawie jakiegoś innego typu, to powinien być to jasny znak, że warto zastanowić się nad wykorzystaniem typu mapowanego. Zyskujesz wtedy gwarancję, że oba te typy będą ze sobą spójne (wprowadzenie zmiany w typie bazowym wprowadzi ją również w typie mapowanym).

Avatar: Wojciech Rygorowicz

Software Engineer / Fullstack developer

Wojciech Rygorowicz

wojciech.rygorowicz@gmail.com

Podziel się na

Dodaj komentarz

Komentarze (0)

Brak komentarzy