Polski

Odkryj strażniki i asercje typów w TypeScript, by wzmocnić bezpieczeństwo, unikać błędów i pisać solidny kod. Ucz się z praktycznymi przykładami.

Opanowanie bezpieczeństwa typów: Kompleksowy przewodnik po strażnikach typów i asercjach typów

W świecie tworzenia oprogramowania, zwłaszcza podczas pracy z językami dynamicznie typowanymi, takimi jak JavaScript, utrzymanie bezpieczeństwa typów może być znaczącym wyzwaniem. TypeScript, nadzbiór JavaScriptu, rozwiązuje ten problem, wprowadzając statyczne typowanie. Jednak nawet w systemie typów TypeScript pojawiają się sytuacje, w których kompilator potrzebuje pomocy w wywnioskowaniu prawidłowego typu zmiennej. Właśnie tutaj do gry wchodzą strażnicy typów (type guards) i asercje typów (type assertions). Ten kompleksowy przewodnik zagłębi się w te potężne funkcje, dostarczając praktycznych przykładów i najlepszych praktyk, aby zwiększyć niezawodność i łatwość utrzymania kodu.

Czym są strażnicy typów?

Strażnicy typów to wyrażenia TypeScript, które zawężają typ zmiennej w określonym zakresie. Pozwalają kompilatorowi zrozumieć typ zmiennej z większą precyzją, niż pierwotnie wywnioskował. Jest to szczególnie przydatne przy pracy z typami unijnymi lub gdy typ zmiennej zależy od warunków wykonania. Używając strażników typów, można unikać błędów w czasie wykonania i pisać bardziej solidny kod.

Powszechne techniki strażników typów

TypeScript dostarcza kilka wbudowanych mechanizmów do tworzenia strażników typów:

Używanie typeof

Operator typeof to prosty sposób na sprawdzenie prymitywnego typu zmiennej. Zwraca on ciąg znaków wskazujący typ.

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // TypeScript wie, że 'value' jest tutaj stringiem
  } else {
    console.log(value.toFixed(2)); // TypeScript wie, że 'value' jest tutaj liczbą
  }
}

printValue("hello"); // Wynik: HELLO
printValue(3.14159); // Wynik: 3.14

Używanie instanceof

Operator instanceof sprawdza, czy obiekt jest instancją określonej klasy. Jest to szczególnie przydatne podczas pracy z dziedziczeniem.

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof!");
  }
}

function makeSound(animal: Animal) {
  if (animal instanceof Dog) {
    animal.bark(); // TypeScript wie, że 'animal' jest tutaj psem (Dog)
  } else {
    console.log("Generic animal sound");
  }
}

const myDog = new Dog("Buddy");
const myAnimal = new Animal("Generic Animal");

makeSound(myDog); // Wynik: Woof!
makeSound(myAnimal); // Wynik: Generic animal sound

Używanie in

Operator in sprawdza, czy obiekt posiada określoną właściwość. Jest to przydatne w przypadku obiektów, które mogą mieć różne właściwości w zależności od ich typu.

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function move(animal: Bird | Fish) {
  if ("fly" in animal) {
    animal.fly(); // TypeScript wie, że 'animal' jest tutaj ptakiem (Bird)
  } else {
    animal.swim(); // TypeScript wie, że 'animal' jest tutaj rybą (Fish)
  }
}

const myBird: Bird = { fly: () => console.log("Flying"), layEggs: () => console.log("Laying eggs") };
const myFish: Fish = { swim: () => console.log("Swimming"), layEggs: () => console.log("Laying eggs") };

move(myBird); // Wynik: Flying
move(myFish); // Wynik: Swimming

Niestandardowe funkcje strażników typów

W bardziej złożonych scenariuszach można zdefiniować własne funkcje strażników typów. Funkcje te zwracają predykat typu, który jest wyrażeniem logicznym używanym przez TypeScript do zawężenia typu zmiennej. Predykat typu ma postać zmienna is Typ.

interface Square {
  kind: "square";
  size: number;
}

interface Circle {
  kind: "circle";
  radius: number;
}

type Shape = Square | Circle;

function isSquare(shape: Shape): shape is Square {
  return shape.kind === "square";
}

function getArea(shape: Shape) {
  if (isSquare(shape)) {
    return shape.size * shape.size; // TypeScript wie, że 'shape' jest tutaj kwadratem (Square)
  } else {
    return Math.PI * shape.radius * shape.radius; // TypeScript wie, że 'shape' jest tutaj kołem (Circle)
  }
}

const mySquare: Square = { kind: "square", size: 5 };
const myCircle: Circle = { kind: "circle", radius: 3 };

console.log(getArea(mySquare)); // Wynik: 25
console.log(getArea(myCircle)); // Wynik: 28.274333882308138

Czym są asercje typów?

Asercje typów to sposób na poinformowanie kompilatora TypeScript, że wiemy więcej o typie zmiennej, niż on jest w stanie zrozumieć. Są one sposobem na nadpisanie wnioskowania typów przez TypeScript i jawne określenie typu wartości. Ważne jest jednak, aby używać asercji typów z ostrożnością, ponieważ mogą one ominąć sprawdzanie typów przez TypeScript i potencjalnie prowadzić do błędów w czasie wykonania, jeśli zostaną użyte nieprawidłowo.

Asercje typów mają dwie formy:

Słowo kluczowe as jest generalnie preferowane, ponieważ jest bardziej kompatybilne z JSX.

Kiedy używać asercji typów

Asercje typów są zazwyczaj używane w następujących scenariuszach:

Przykłady asercji typów

Jawna asercja typu

W tym przykładzie dokonujemy asercji, że wywołanie document.getElementById zwróci HTMLCanvasElement. Bez tej asercji TypeScript wywnioskowałby bardziej ogólny typ HTMLElement | null.

const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const ctx = canvas.getContext("2d"); // TypeScript wie, że 'canvas' jest tutaj elementem HTMLCanvasElement

if (ctx) {
  ctx.fillStyle = "#FF0000";
  ctx.fillRect(0, 0, 150, 75);
}

Praca z typami nieznanymi (unknown)

Podczas pracy z danymi z zewnętrznego źródła, takiego jak API, możesz otrzymać dane o nieznanym typie. Możesz użyć asercji typu, aby poinformować TypeScript, jak ma traktować te dane.

interface User {
  id: number;
  name: string;
  email: string;
}

async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  const data = await response.json();
  return data as User; // Asercja, że dane to obiekt typu User
}

fetchUser(1)
  .then(user => {
    console.log(user.name); // TypeScript wie, że 'user' jest tutaj obiektem typu User
  })
  .catch(error => {
    console.error("Błąd podczas pobierania użytkownika:", error);
  });

Ostrzeżenia dotyczące używania asercji typów

Asercje typów powinny być używane oszczędnie i z ostrożnością. Nadużywanie asercji typów może maskować podstawowe błędy typów i prowadzić do problemów w czasie wykonania. Oto kilka kluczowych kwestii:

Zawężanie typów (Type Narrowing)

Strażnicy typów są nierozerwalnie związani z koncepcją zawężania typów (type narrowing). Zawężanie typów to proces doprecyzowywania typu zmiennej do bardziej szczegółowego typu na podstawie warunków lub sprawdzeń w czasie wykonania. Strażnicy typów to narzędzia, których używamy do osiągnięcia zawężania typów.

TypeScript używa analizy przepływu sterowania, aby zrozumieć, jak typ zmiennej zmienia się w różnych gałęziach kodu. Kiedy używany jest strażnik typu, TypeScript aktualizuje swoje wewnętrzne rozumienie typu zmiennej, co pozwala na bezpieczne używanie metod i właściwości specyficznych dla tego typu.

Przykład zawężania typów

function processValue(value: string | number | null) {
  if (value === null) {
    console.log("Value is null");
  } else if (typeof value === "string") {
    console.log(value.toUpperCase()); // TypeScript wie, że 'value' jest tutaj stringiem
  } else {
    console.log(value.toFixed(2)); // TypeScript wie, że 'value' jest tutaj liczbą
  }
}

processValue("test"); // Wynik: TEST
processValue(123.456); // Wynik: 123.46
processValue(null); // Wynik: Value is null

Najlepsze praktyki

Aby efektywnie wykorzystać strażników typów i asercje typów w swoich projektach TypeScript, rozważ następujące najlepsze praktyki:

Kwestie międzynarodowe

Podczas tworzenia aplikacji dla globalnej publiczności, należy pamiętać, jak strażnicy typów i asercje typów mogą wpływać na procesy lokalizacji i internacjonalizacji (i18n). W szczególności należy wziąć pod uwagę:

Podsumowanie

Strażnicy typów i asercje typów to niezbędne narzędzia do zwiększania bezpieczeństwa typów i pisania bardziej solidnego kodu w TypeScript. Rozumiejąc, jak efektywnie korzystać z tych funkcji, można zapobiegać błędom w czasie wykonania, poprawiać łatwość utrzymania kodu i tworzyć bardziej niezawodne aplikacje. Pamiętaj, aby w miarę możliwości preferować strażników typów nad asercjami typów, dokumentować swoje asercje i walidować dane zewnętrzne, aby zapewnić dokładność informacji o typach. Stosowanie tych zasad pozwoli Ci tworzyć bardziej stabilne i przewidywalne oprogramowanie, gotowe do wdrożenia na całym świecie.