Paweł Łukasiewicz
2024-12-02
Paweł Łukasiewicz
2024-12-02
Udostępnij Udostępnij Kontakt
Wprowadzenie

Funkcje asynchroniczne są kluczowym elementem nowoczesnych aplikacji, szczególnie w pracy z operacjami, które wymagają pewnego czasu, jak np. zapytania do API czy praca z bazami danych. TypeScript, dzięki swojemu systemowi typów, pozwala precyzyjnie określać, co zwracają funkcje asynchroniczne, co pomaga programistom lepiej zrozumieć, czego mogą się spodziewać po kodzie.

W tym wpisie omówimy, jak poprawnie typować funkcje asynchroniczne w TypeScript, z naciskiem na funkcje zwracające obietnice (Promises).

Co to jest Promise?

Obietnica (Promise) w JavaScript (i TypeScript) to obiekt reprezentujący zakończoną lub trwającą operację asynchroniczną oraz jej wynik. Może zakończyć się sukcesem (z wynikiem) lub niepowodzeniem (z błędem).

Obietnice mają trzy możliwe stany:

  • Pending (Oczekujący): operacja jeszcze się nie zakończyła.
  • Fulfilled (Zrealizowany): operacja zakończyła się sukcesem i zwraca wynik.
  • Rejected (Odrzucony): operacja zakończyła się niepowodzeniem i zwraca błąd.

Przykład funkcji zwracającej obietnicę:

function getData(): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve("Dane pobrane");
    }, 1000);
  });
}
W powyższym przykładzie funkcja getData zwraca obietnicę, która po 1 sekundzie zwróci string "Dane pobrane".

Typowanie obietnic w TypeScript

W TypeScript możemy precyzyjnie określać typ wartości, którą obietnica zwróci po zakończeniu operacji. Używamy do tego typu generycznego Promise<T>, gdzie T reprezentuje typ zwracanej wartości.

Przykład:

function fetchData(): Promise<number> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(42);
    }, 1000);
  });
}
Tutaj funkcja fetchData zwraca obietnicę, która po zakończeniu operacji zwróci wartość typu number. Dzięki takiemu typowaniu, TypeScript będzie wiedział, jakiego typu danych możemy się spodziewać w wyniku spełnienia obietnicy.

Typowanie funkcji asynchronicznych z użyciem async/await

TypeScript w pełni wspiera składnię async/await, która pozwala na bardziej zwięzłe i czytelne zarządzanie obietnicami. Funkcje oznaczone jako async zawsze zwracają obietnicę.

Przykład:

async function getNumber(): Promise<number> {
  return 42;
}
Tutaj funkcja getNumber jest oznaczona jako asynchroniczna (async) i zwraca obietnicę, która zwróci wartość typu number.

Typowanie funkcji zwracających różne typy obietnic

W praktyce asynchroniczne funkcje mogą zwracać różne typy wyników, takie jak obiekty, tablice lub inne typy. Możemy dokładnie określić typ obietnicy, którą zwraca funkcja, co zwiększa czytelność i niezawodność kodu.

Przykład z obietnicą zwracającą tablicę:

async function fetchItems(): Promise<string[]> {
  return ["item1", "item2", "item3"];
}
Tutaj funkcja fetchItems zwraca obietnicę, która po zakończeniu operacji zwróci tablicę stringów.

Przykład z obietnicą zwracającą obiekt:

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

async function fetchUser(): Promise<User> {
  return { id: 1, name: "John Doe" };
}
Funkcja fetchUser zwraca obietnicę, która zwróci obiekt o strukturze zgodnej z interfejsem User.

Łączenie obietnic o różnych typach

Czasami będziemy pracować z kilkoma obietnicami, które zwracają różne typy wartości. W takim przypadku możemy korzystać z funkcji takich jak Promise.all, które pozwalają łączyć wyniki wielu obietnic, a TypeScript może automatycznie wywnioskować typ wyniku.

Przykład:

async function fetchData(): Promise<[string, number]< {
  const [message, value] = await Promise.all([
    Promise.resolve("Witaj"),
    Promise.resolve(42)
  ]);

  return [message, value];
}
Tutaj funkcja fetchData zwraca obietnicę, która po zakończeniu zwróci krotkę zawierającą string i liczbę.

Obsługa błędów w funkcjach asynchronicznych

Gdy pracujemy z obietnicami, musimy obsługiwać potencjalne błędy. W TypeScript możemy używać bloku try...catch do obsługi błędów w funkcjach asynchronicznych.

Przykład:

async function getData(): Promise {
  try {
    const data = await fetch("https://api.example.com/data");
    if (!data.ok) {
      throw new Error("Błąd w trakcie pobierania danych");
    }
    return await data.json();
  } catch (error) {
    throw new Error(`Nie udało się pobrać danych: ${error}`);
  }
}
W tym przypadku funkcja getData próbuje pobrać dane z API, a jeśli coś pójdzie nie tak (np. problem z siecią), rzuca wyjątek. Typowany wynik to Promise, co oznacza, że po zakończeniu obietnicy oczekujemy stringa.

Typowanie funkcji asynchronicznych z obietnicami wielokrotnymi

Możemy także tworzyć funkcje, które zwracają różne typy wyników w zależności od warunków. TypeScript pozwala na dokładne określenie typów dla takich sytuacji.

Przykład:

async function fetchData(flag: boolean): Promise {
  if (flag) {
    return "Dane pobrane";
  } else {
    return 42;
  }
}
Tutaj funkcja fetchData może zwracać albo string, albo liczbę, w zależności od wartości przekazanego parametru flag. TypeScript dzięki temu zapewnia, że obsługujemy oba możliwe typy wyników.

Typ Promise

Funkcje asynchroniczne nie zawsze muszą zwracać wartość. W TypeScript możemy typować takie funkcje jako Promise<void>, co oznacza, że obietnica nie zwróci żadnej wartości po zakończeniu operacji.

Przykład:

async function logMessage(): Promise {
  console.log("Wiadomość zalogowana.");
}
Tutaj funkcja logMessage nie zwraca żadnej wartości, a obietnica zakończy się bez wyniku.

Podsumowanie

Typowanie funkcji asynchronicznych w TypeScript pozwala na bardziej precyzyjne kontrolowanie, jakie wyniki zwracają obietnice, co zwiększa niezawodność kodu. Kluczowe aspekty typowania funkcji asynchronicznych obejmują:

  • Użycie typu generycznego Promise<T> do określenia typu wartości zwracanej przez obietnicę.
  • Typowanie funkcji async za pomocą typu Promise.
  • Łączenie wielu obietnic o różnych typach i obsługa wyników z użyciem np. Promise.all.
  • Obsługa błędów w funkcjach asynchronicznych z użyciem bloku try...catch.
  • Tworzenie funkcji zwracających różne typy wyników, co pozwala na bardziej elastyczne podejście do obsługi operacji asynchronicznych.