Type Guards (strażnicy typów) to technika w TypeScript, która pozwala nam na dynamiczne sprawdzanie i zapewnienie poprawności typów w czasie wykonywania programu. Dzięki temu możemy bezpiecznie obsługiwać różne typy, zapewniając, że wykonywany kod jest zgodny z oczekiwanym typem zmiennej. Jest to szczególnie przydatne, gdy pracujemy z typami łączonymi (union types) lub kiedy musimy mieć pewność, że obiekt spełnia określone kryteria typów.
Czym są Type Guards?
Type Guards to specjalne konstrukcje w kodzie, które pozwalają nam sprawdzić typ zmiennej w czasie działania programu i, na podstawie tego sprawdzenia, wykonać odpowiednią logikę. Umożliwia to kompilatorowi zawężenie typu zmiennej do bardziej konkretnego typu w danym bloku kodu.
TypeScript dostarcza kilka sposobów definiowania strażników typów, takich jak:
Operatory typeof i instanceof;
Customowe funkcje typu "type guard";
Operatory in.
'typeof' jako Type Guard
Operator typeof to jeden z najprostszych i najczęściej używanych sposobów na sprawdzanie podstawowych typów zmiennych w JavaScript, a także w TypeScript. Jest szczególnie przydatny do pracy z typami prostymi, takimi jak string, number, czy boolean.
W powyższym przykładzie, typeof pozwala nam sprawdzić, czy value jest stringiem czy number, a następnie wywołać odpowiedni kod na podstawie tego typu.
'instanceof' jako Type Guard
Kiedy chcemy sprawdzić, czy obiekt jest instancją określonej klasy, możemy użyć operatora instanceof. Jest to przydatne, gdy pracujemy z bardziej złożonymi typami, jak obiekty lub klasy.
Przykład:
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function animalSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
let myDog = new Dog();
let myCat = new Cat();
animalSound(myDog); // Wypisze: Woof!
animalSound(myCat); // Wypisze: Meow!
Tutaj, instanceof pozwala nam sprawdzić, czy obiekt animal jest instancją klasy Dog czy Cat. Dzięki temu możemy wywołać odpowiednią metodę, która jest dostępna tylko dla określonego typu obiektu.
'in' jako Type Guard
Operator in jest używany do sprawdzania, czy obiekt ma określoną właściwość. To przydatne, gdy chcemy upewnić się, że dana właściwość istnieje w obiekcie, zanim z niej skorzystamy.
Przykład:
interface Car {
drive: () => void;
}
interface Boat {
sail: () => void;
}
function operateVehicle(vehicle: Car | Boat) {
if ("drive" in vehicle) {
vehicle.drive();
} else {
vehicle.sail();
}
}
let car: Car = { drive: () => console.log("Driving") };
let boat: Boat = { sail: () => console.log("Sailing") };
operateVehicle(car); // Wypisze: Driving
operateVehicle(boat); // Wypisze: Sailing
W tym przykładzie, in sprawdza, czy obiekt vehicle ma właściwość drive, co pozwala określić, czy jest to Car, czy Boat. W zależności od wyniku, możemy bezpiecznie wywołać odpowiednią metodę.
Customowe Type Guards (Funkcje strażników typów)
Możemy także tworzyć własne funkcje, które sprawdzają typy. TypeScript umożliwia definiowanie funkcji typu "type guard" za pomocą specjalnej sygnatury param is Type, co pozwala na zawężenie typu w kodzie.
Przykład:
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function isFish(animal: Fish | Bird): animal is Fish {
return (animal as Fish).swim !== undefined;
}
function animalActivity(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim();
} else {
animal.fly();
}
}
let fish: Fish = {
swim: () => console.log("Swimming")
};
let bird: Bird = {
fly: () => console.log("Flying")
};
animalActivity(fish); // Wypisze: Swimming
animalActivity(bird); // Wypisze: Flying
W tym przykładzie funkcja isFish sprawdza, czy obiekt animal jest Fish, i zwraca true, jeśli posiada metodę swim. Dzięki tej funkcji możemy bezpiecznie wywołać odpowiednie metody wewnątrz funkcji animalActivity, wiedząc, że mamy do czynienia z odpowiednim typem.
Type Guards dla typów złożonych
Type Guards sprawdzają się także w przypadku bardziej złożonych typów, takich jak typy łączone (union types) i typy przecięte (intersection types). Umożliwiają one dynamiczne zawężanie typu i odpowiednią reakcję na różne struktury danych.
Przykład z typem łączonym:
type Admin = {
name: string;
role: "admin";
}
type User = {
name: string;
email: string;
}
function isAdmin(person: Admin | User): person is Admin {
return (person as Admin).role === "admin";
}
function getDetails(person: Admin | User) {
if (isAdmin(person)) {
console.log(`Admin: ${person.name}`);
} else {
console.log(`User: ${person.name}, Email: ${person.email}`);
}
}
let admin: Admin = { name: "Jan", role: "admin" };
let user: User = { name: "Adam", email: "adam@example.com" };
getDetails(admin); // Wypisze: Admin:
Jan getDetails(user); // Wypisze: User: Adam, Email: adam@example.com
Funkcja isAdmin sprawdza, czy obiekt ma pole role, co pozwala nam zawęzić typ person do Admin. Dzięki temu możemy w bloku if bezpiecznie odwoływać się do pól dostępnych tylko dla tego typu.
Podsumowanie
Type Guards są niezwykle użyteczne w TypeScript, pozwalając na dynamiczne sprawdzanie i zawężanie typów w trakcie działania programu. Dzięki nim możemy pracować z bardziej złożonymi typami, jak typy łączone, i zapewniać, że operacje na danych są bezpieczne i zgodne z typem. Type Guards zwiększają bezpieczeństwo i przejrzystość kodu, minimalizując ryzyko błędów związanych z nieprawidłowym użyciem typów.
Najczęściej stosowane techniki Type Guards to:
typeof – do sprawdzania prostych typów (string, number, boolean).
instanceof – do sprawdzania, czy obiekt jest instancją danej klasy.
in – do sprawdzania, czy obiekt posiada określoną właściwość.
Customowe funkcje typu "type guard" – do definiowania bardziej złożonych strażników typów, które zawężają typy na podstawie logicznych warunków.