KamilTroczewski

Typy które ciężko zrozumieć w Typescripcie

💡

Ten artykuł jest też dostępny w języku angielskim. Jeżeli chcesz to

Praca z Typescriptem jest bardzo przyjemna. Sam w sobie dużo rzeczy ułatwia i pozwala pisać kod, który na pewno nie dopuści, aby doszło do niedozwolonych operacji. Wyciąga do nas dłoń i chętnie podpowiada nam jaki typ zwraca dana funkcja. Natomiast w naszym interesie jest, aby program był jak najbardziej przygotowany na potencjalne zagrożenia. Dlatego tak ważne jest przypisywanie poprawnego typu funkcjom i zmiennym, co jednak czasami nie jest to takie proste. Mimo to, piszę ten artykuł żeby móc utrwalić swoją wiedzę, a przy okazji może Tobie też pomoże to zrozumieć lepiej niektóre typy w Typescripcie 👀

Typ void

Typ void moźe przyjąć wartość null lub undefined. Typescript wykorzystuje void jako typ zwracany przez funkcję. Być może zapytasz: "ale jak to funkcja która nie ma słowa kluczowego return coś zwraca?". Tak, sama funkcja która z domysłu nic nie powinna zwracać, zwraca właśnie typ void. Jest to związane nie tyle co z Typescriptem, ale z samym Javascriptem, który, w wypadku gdy nie ma żadnego słowa kluczowego return, zwraca właśnie undefined.

const undefinedValue: void = undefined;
const nullValue: void = null;

const voidFunction = () => { // () => void 
  console.log('Ja nic nie zwracam 🙄');
};
💡

Próba przypisania wartości może się nie udać jeżeli flaga strictNullChecks jest włączona w tsconfig.json. Bardzo zachęcam pozostawic te flagę włączoną, ponieważ chroni ona przed przypisaniem wartości null i undefined, tam gdzie nie powinno to być możliwe. Typescript lepiej też wtedy podpowiada typy. Natomiast, na potrzebę tego przykładu, wyłączyłem tę flagę.

Typ never

Typ never jest typem, który nie istnieje. Jedyne, co do niego możemy przypisać, to samego siebie - czyli never. Tak więc, kiedy taki typ jest użyteczny, jeżeli tak naprawdę nic nie może przyjąć? Zazwyczaj sami tego typu nie przypisujemy, chyba że piszemy bibliotekę i mamy w niej jakiś typ warunkowy, o czym wspomnę poźniej. Najczęściej jednak Typescript domyśla się kiedy wystąpi typ never. Łatwiej to będzie przedstawić na przykładzie:

const functionWithNeverBlock = (value: number) => {
  if (typeof value === 'string') {
    const neverValue = value; // never
  }
  const numberValue = value; // number
}

Ten przykład może wydawać się trochę absurdalny. Z góry przecież wiemy, że nasz argument jest liczbą. Natomiast, właśnie na takie przykłady jest przygotowany typ never. Mimo, że to co robimy jest technicznie możliwe, tak naprawdę ta wartość jest niczym, ponieważ kod w tej klamrze się po prostu nie wykona. Innym przykładem są funkcje, które nigdy nie skończą się wykonywać, albo takie, które nic nie robią poza zgłaszaniem błędu.

const throwError = () => { // () => never
  throw new Error('Ja tu tylko zgłaszam error ;/');
}

const inifiniteLoop = () => { // () => never
  while (true) {
    console.log('Nigdy nie mów nigdy 😜');
  }
}

Wszystkie jak dotąd wymienione przykłady otrzymały typ never tylko dlatego, że sam Typescript tak się domyślił. Stąd pytanie, kiedy może być sensowne użycie tego typu? Bardzo często wykorzystuje się go przy typach warunkowych. Być może nigdy się jeszcze z tym nie zetknąłeś w Typescripcie, natomiast składnia jest bardzo podobna do operatora trójargumentowego w Javascripcie (ang. ternary operator).

type StringOrNumber<T> = T extends string ? string : number;
type StringType = StringOrNumber<''> // string

Powyższy przykład przedstawia typ warunkowy, który sprawdza, czy nasz typ jest liczbą czy stringiem. Bardziej sprawne oko zauważy jednak, że ten kod, tak naprawdę ma bardzo dużą wadę. Będzie działał świetnie, gdy jako typ, podamy mu stringa bądź liczbę. Natomiast, co w wypadku, gdy podamy tam coś innego? Dla przykładu, umieśćmy tam typ boolean.

type ShouldBeNeverType = StringOrNumber<boolean> // number

Jak widać, nasz typ przyjął wartość number, co jest nieprawidłowe! Powinien przyjąć raczej naszą omawianą wartość never. Jak to zrobić?

type StringOrNumber<T> = T extends string ? string : T extends number ? number : never;
type ShouldBeNeverType = StringOrNumber<boolean> // never

Właśnie w taki sposób. Ten operator warunkowy sprawdza, czy jego typ jest stringiem lub liczbą. W przeciwnym wypadku, ustawia go na never.

Typ any

Jest to najbardziej kojarzony typ, z wszystkich typów, które opisuje w tym artykule. Do niego można przypisać każdą wartość, która jest poprawna w Javascripcie. Dlatego, trzeba na niego bardzo uważać. Taka wolność nie jest dobra. any powinno się używać bardzo świadomie i tylko w ostatecznych przypadkach. Bardzo często any wykorzystuje się w sytuacjach, gdy przepisuje się kod z Javascriptu do Typescriptu. Jednak z czasem rozwoju te typy zamienia się na bardziej specyficzne.

let anyValue: any = 7312;
anyValue = 'moge byc stringiem';
anyValue = { lub: 'moge byc obiektem' };
anyValue = ['nawet', 'tablica'];

To, co dzieje się wyżej, jest bardzo niebezpieczne. Gdy spróbujemy odwołać się do klucza obiektu, który w tym momencie może być liczbą, Typescript nie będzie na nas krzyczał i nie wskaże nam błędu. Używając any niejako mówimy typescriptowi: "Słuchaj Typescript, nie bój się, dałem any i ja to ogarnę". Dlatego, ten typ trzeba omijać. A co zrobić w wypadku, gdy naprawdę nie wiemy, jaki typ będzie miała nasza wartość?

Typ unknown

Tak jak w any, do typu unknown można przypisać każdą wartość. Natomiast, samego typu unknown nie można przypisywać do innych typów. Brzmi to trochę jak naukowy bełkot. Przykład na pewno pomoże w zrozumieniu:

let unknownValue: unknown =  7312;
unknownValue = 'moge byc stringiem';
unknownValue = { lub: 'moge byc obiektem' };
unknownValue = ['nawet', 'tablica'];

const errorUnknown: number = unknownValue; // Error
const numberValue: number = typeof unknownValue === 'number' ? unknownValue : 0; // unknownValue || 0

Jak widać, do typu unknown przypisaliśmy takie same wartości, jak wcześniej do typu any. Natomiast, gdy chcemy tą zmienną przypisać do zmiennej o innym typie, to mamy błąd. Musimy najpierw sprawdzić, czy zmienna jest poprawnego typu w trakcie działania programu. Dlatego, unknown wprowadza duży stopień bezpieczeństwa i pozwola nam spać troszkę lepiej.

Typ unknown, bardzo często jest wykorzystywany przy łapaniu błędów w bloku catch. Nigdy nie mamy przecież pewności, jakiego typu jest nasz error. Być może jest stringiem, a może obiektem, a może jeszcze czymś innym. Zamiast zgadywania, lepiej już wcześniej poprawnie otypować sobie error i na jego zasadzie sprawdzać możliwe przypadki.

const thisThrowsError = async () => {
  try {
    await likelyToBreak();
  } catch (err: unknown) {
    if (err instanceof Error) {
      return console.error(err.message);
    }
    console.error('Oops, coś poszło nie tak');
  }
}

Podsumowanie

Nie ukrywam, że informacji odnośnie typów trochę jest. Omówiliśmy cztery typy: void, never, any i unknown. Każdy z nich ma różne zastosowanie, lecz czy zawsze wiadomo, jaki typ powinniśmy przypisać w różnych sytuacjach? Zwykle tak, ale trzeba być świadomym ich działania, różnic, zastosowania i istnienia.