Javascript

TypeScript – trochę więcej o typach, część 1

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”.