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.