W pierwszym artykule dotyczącym języka TypeScript przedstawiłem podstawowe zagadnienia związane z typami danych. Dzisiaj zaprezentuję kilka dodatkowych, równie istotnych funkcjonalności, które przekazane zostały w ręce programistów.
Union Types
Niejednokrotnie – co ściśle związane jest z dynamiczną naturą języka JavaScript – decydowaliśmy się tworzyć funkcje, które przyjmują różne typy danych, a w zależności od przekazanych argumentów różniło się ich działanie.
W tym przypadku w TypeScript możemy skorzystać z unii. Aby zadeklarować zmienną, która może przyjmować jednocześnie typ string
i number
, wykorzystujemy poniższą konstrukcję:
let stringOrNumber: string | number;
stringOrNumber = 1;
stringOrNumber = "cat";
Lepiej działanie unii prezentuje poniższy, dość trywialny przykład w postaci fragmentu kodu:
interface Dog { woof() }
interface Cat { meow() }
function speak(animal: Cat | Dog) {
if (animal.woof) {
animal.woof();
return;
}
animal.meow();
}
W zależności od typu przekazanego argumentu – chcemy wywołać odpowiednią metodę. Czy kompilacja się powiedzie? Nie, mimo, że kod prawdopodobnie zadziała poprawnie. W konsoli (IDE może zrobić to jeszcze szybciej) zobaczymy następujący komunikat błędu:
Property ‚woof’ does not exist on type ‚Dog | Cat’. Property ‚woof’ does not exist on type ‚Cat’.
Transpiler nie był w stanie sam wydedukować typu, mimo, że umieściliśmy w kodzie instrukcję warunkową, która w jawny sposób sprawdza składowe obiektów.
Co w tym przypadku powinniśmy zrobić? Wystarczy skorzystać z mechanizmu zwanego type guards. Stwórzmy funkcję, której zadaniem będzie sprawdzenie typu obiektu:
function isDog(animal: Cat | Dog): animal is Dog {
return typeof (<Dog>animal).woof !== 'undefined';
}
To wszystko. Teraz, gdy przekształcimy nasz kod do następującej postaci:
function speak(animal: Cat | Dog) {
if (isDog(animal)) {
animal.woof();
return;
}
animal.meow();
}
Wszystko zadziała poprawnie.
Intersection Types
Rozważmy poniższy fragment kodu.
interface Weapon { hit() }
interface Serializable {
readObject();
writeObject();
}
let knife: Weapon & Serializable;
Oznacza on, że obiekt przypisany do zmiennej knife
musi być zgodny zarówno z typem Weapon
, jak i Serializable
. Oczywiście – nie ma ograniczeń w ilości typów, które chcemy wykorzystać. Poniższy zapis jest również jak najbardziej poprawny.
let knife: Weapon & Serializable & Cloneable;
Aliasy
Bardzo wygodnym mechanizmem, który pozwala nam ograniczyć ilość powtarzanego kodu związanego z typami, a jednocześnie sprawić, że będzie on łatwiejszy w zrozumieniu jest możliwość tworzenia aliasów. Przykładowo, aby uniknąć powtarzania poniższej konstrukcji:
let knife: Weapon & Serializable;
Możemy utworzyć alias:
type SerializableWeapon = Weapon & Serializable;
let knife: SerializableWeapon;
Kolejnym przykładem jest wykorzystanie aliasu dla poniższego typu:
type DeferredString = Promise<string> | Observable<string>;
Znacznie łatwiej zaprezentować przypadki użycia aliasów wykorzystując typy generyczne, o których w kolejnym artykule. Na ten moment warto uświadomić sobie, że taka możliwość po prostu istnieje.
String Literal Types
Interesującym dodatkiem związanym z typami są literały typu string
. Dzięki nim, możemy dokładnie określić warianty jakie przyjmować musi dana zmienna. Przykładowo, poniższy zapis oznacza, że zmienna direction
będzie mogła przyjmować jedynie następujące wartości – left, right, above oraz below:
let direction: "left" | "right" | "above" | "below";
Poniższa próba przypisania innej wartości (np. „value”), zakończy się komunikatem błędu:
Type ‚”value”‚ is not assignable to type ‚”left” | „right” | „above” | „below”‚.
Literały możemy traktować jak swego rodzaju enumeratory typu string
. W tym wypadku ograniczają one zakres dostępnych wartości powiązanych pewną dziedziną. To rozwiązanie doskonale sprawdza się z aliasami:
type Direction = "left" | "right" | "above" | "below";
let direction: Direction;
Podsumowanie
W dzisiejszym wpisie przedstawiłem cztery użyteczne dodatki dotyczące stosowania typów przy użyciu TypeScript’a. Kolejny wpis będzie poruszał tematykę typów generycznych, a po nich – część druga serii „trochę więcej o typach”.