W ostatnich dwóch postach z serii, przedstawiłem działanie typów generycznych w TypeScript. Zagadnienie to powinno być już w miarę jasne a korzyści płynące z korzystania z generyków oczywiste dla każdego.
Jednak jeżeliby się nad tym chwilę zastanowić, szybko dojdziemy do wniosku, że pozwalanie metodom lub klasom na korzystanie z dowolnych rodzajów typów może czasem sprawiać problemy. Dobrze byłoby czasem dopuszczać tylko niektóre typy, a inne nie. Dziś przedstawię rozwiązanie dla tego problemu
Wyobraźmy sobie następującą sytuację. W moim kodzie posiadam dwie klasy reprezentujące pewne zwierzęta. Wewnątrz obiektu znajduje się informacja o tym ile łap ma dane zwierze:
1 2 3 4 5 6 7 8 9 10 |
class Dog { constructor(public name:string){}; sound:string = "Woof!" paws:number = 4; } class mutatedCat { constructor(public name:string){}; paws:number = 5; } |
Teraz chciałbym stworzyć funkcje, która przyjmuje dwie instancje którejś z powyższych klas i sumuję ilość łap. Mogę zrobić to w następujący sposób:
1 2 3 |
function sumOfPaws(x:{paws:number},y:{paws:number}):number{ return x.paws + y.paws; } |
Zapis, który wykorzystałem w liście parametrów oznacza, że x oraz y mają być typu obiektu, który posiada pole paws, przechowujące dane typu liczbowego. Teraz nic nie stoi na przeszkodzie, aby stworzyć instancje klas zwierząt i pododawać ich łapy. A przynajmniej tak by się wydawało. W specyfikacji projektu, zostało zapisane, że funkcja sumOfPaws może sumować ilość łap tylko tych samych zwierząt. W tej chwili nic nie stoi na przeszkodzie żeby jako pierwszy parametr przekazać obiekt psa a drugi obiekt zmutowanego kota. Mój program nie powinien pozwalać na takie zachowanie.
Jeżeli rozwiązanie, które przychodzi Ci do głowy to typy generyczne, to dobrze kombinujesz. Jeżeli w funkcji sumOfPaws określę jeden parametr typowy i przypiszę go do x oraz y, wtedy oba będą musiały być takie same:
1 2 3 4 |
//BŁĘDNY PRZYKŁAD function sumOfPaws<T>(x:T,y:T):number{ return x.paws + y.paws; } |
Teraz zarówno x jak i y muszą być takiego samego typu. Niestety, takie rozwiązanie powoduje, że TypeScript wyrzuca błąd:
Ponieważ T może reprezentować dowolny typ, nie ma żadnej pewności, że będzie on posiadał w sobie pole paws. Trochę lipa. Z pozoru jest to sytuacja bez wyjścia, ale na szczęście istnieje sposób aby rozwiązać ten problem: Ograniczenia typów generycznych.
Jak to zrobić? Oto przykład:
1 2 3 4 5 6 7 8 9 10 11 12 |
function sumOfPaws<T extends {paws:number}>(x:T,y:T):number{ return x.paws + y.paws; } var maja = new Dog("maja"); var milus = new Dog("milus"); var cat1 = new MutatedCat("specimen01"); var cat2 = new MutatedCat("specimen02"); sumOfPaws<Dog>(maja,milus); sumOfPaws<MutatedCat>(cat1,cat2); sumOfPaws<Dog>(maja,cat2) // Zgłasza błąd, sumOfPaws przyjmuje tylko argumenty typu Dog |
Cały sekret znajduje się w pierwszej linijce kodu. Wewnątrz listy parametrów typowych, do typu, który chcę ograniczyć dodaję operator extends, zupełnie jakbym dodawał do niego interfejs. Następnie dodaje obiekt, który definiuje ograniczenie dla T. Teraz implementować T, mogą tylko obiekty, które posiadają pole paws.
Pod definicją funkcji, podaję parę przykładów, które udowadniają, że wszystko działa tak jak trzeba. W ten prosty sposób można nałożyć na nasz program jeszcze większa kontrole. Kod pisany w ten sposób, może rozrastać się do naprawdę sporych rozmiarów, a jednak wciąż będzie łatwy do ogarnięcia.
Może nasunąć się pytanie, co jeżeli ograniczenie typu będzie obszerniejsze. Bo może takie być, obiekt którym ograniczam typ generyczny może mieć wiele pól. Zapis takiego tworu może być nieporęczny. No i co jeżeli tym samym obiektem chcę ograniczać różne typy, to może doprowadzić do przepisywania kodu. I na to jest rozwiązanie Co ciekawe do ograniczeń typów generycznych możemy używać interfejsów.
Powyższy przykład wystarczy przepisać w ten sposób:
1 2 3 4 5 6 7 |
interface objectWithPaws { paws:number; } function sumOfPaws<T extends objectWithPaws>(x:T,y:T):number{ return x.paws + y.paws; } |
Prawda że dużo wygodniejsze. Interfejs oczywiście też może mieć więcej pól i wszystkie będą brane pod uwagę podczas ograniczania generyka.
To już wszystko jeśli chodzi o typy generyczne. Mam nadzieję, że udało mi się przekazać w miarę jasno to jak działają i jakie korzyści płyną z używania ich w programach TypeScriptowych.
Kolejnym mechanizmem TSa, który przedstawię w przyszłych postach będą modły. Jeżeli chcesz być na bieżąco z postami na blogu zachęcam do polubienia mojej strony na facebooku. Zawsze zamieszczam tam informacje o wszystkich nowościach. Jest to też dobre miejsce na kontakt ze mną. Na wszystkie pytania zawsze odpowiem :).