Załóżmy, że mam sobie komponent App, który coś tam wyświetla.
export const App = () => {
return (
<ColorPicker />
);
}
Żeby nie pisać pełnej implementacji tego komponentu ColorPicker, to jedynie spróbuję przekonać TypeScript, że taki komponent na 100% jest i że ma wyluzować.
import { FC } from "react";
declare const ColorPicker: FC;
Nie będę tu nic odpalał zajmiemy się jedynie pisaniem TypeScripta.
Chcę jeszcze, aby komponent ColorPicker przyjmował jeszcze jakieś props.
Zastanowię się przez chwilę, co mój komponent ma robić. Jaki ma być jego interfejs.
interface ColorPickerProps {}
Mój wybieracz kolorów ma przyjmować kolory, niech to będzie tablica. Ale nie byle jakie kolory. Niech to będzie zbiór trzech kolorów.
type Color = 'red' | 'green' | 'blue';
interface ColorPickerProps {
colors: Array<Color>;
}
Druga ważna sprawa. Chciałbym powiedzieć temu ColorPickerowi, co ma się starć kiedy zostanie wybrany jakiś kolor.
interface ColorPickerProps {
colors: Array<Color>;
onPickColor: (color: Color) => void;
}
Wpisuję tu funkcję ze strzałką, bo funkcje strzałkowe są fajne.
Używanie komponentu ColorPicker
Teraz TS mi krzyczy, że brakuje propsów. Najpierw przekażę listę kolorów. Zakładam, że chcę pokazać picker z możliwością wyboru jednego z dwóch kolorów.
export const App = () => {
return (
<ColorPicker
colors={['red', 'green']}
/>
);
}
Następnie tworzę callback. Funkcję która zareaguje na wybór koloru. Ta funkcja przyjmuje jeden z możliwych kolorów, tych przekazanych w tablicy colors.
Zrobię pierwsze lepsze, co przyjdzie mi do głowy, a więc niech funkcja tylko wyświetli dane w konsoli. Normalnie będzie to coś bardziej fikuśnego, ale przecież tego kodu i tak nie będziemy
odpalać.
const onColor = (color: 'red' | 'green') => {
console.log({ color })
};
OK. To wpuśćmy tego osobnika do komponentu ColorPicker jako prop onPickColor. Ups… TypeScriptowi coś się nie podoba. Sprawdzam, jeszcze raz i wydaje się, że wszystko jest bardzo dobrze przemyślane.
Wydaje się, że jest wszystko ok. Funkcja może przyjąć dokładnie takie same kolory, które przekazuje do tablicy. W props wszędzie mamy nadzbiór tych kolorów z typu Color. Więc mój zbiór mieści się w kryteriach.
Funkcje są kontrawariantne
Tak, tylko funkcja to rozumie inaczej w odniesieniu do przyjmowanego argumentu. Zanim zaczniesz zrzędzić, że TS jest tak samo dziwny jak JS, to daj mi coś wytłumaczyć 🥲
Przyjrzyjmy się komunikatowi błędu:
Type '(color: 'red' | 'green') => void' is not assignable to type '(color: Color) => void'.
Types of parameters 'color' and 'color' are incompatible.
Type 'Color' is not assignable to type '"red" | "green"'.
Type '"blue"' is not assignable to type '"red" | "green"'
Wydaje się jakby TS rozumiał to w drugą stronę. Przecież my to widzimy tak, że to typ 'red' | 'green'
ma się zawierać w Color, a nie odwrotnie. Przecież tak to działa w przekazanej tablicy. Tam TypeScript nie pluje się, że brakuje koloru blue. W funkcji natomiast mu to przeszkadza.
Dlaczego TypeScript przypisuje odwrotnie?
Dzieje się tak, bo argument funkcji stoi na pozycji kontrawariantnej. Co to WTF znaczy „pozycja kontrawariantna”? Nie będę tego tłumaczył w tym odcinku. Na tym kanale nie miało być akademickiego bełkotu, ale jak chcecie wiedzieć, dajcie znać w komentarzach, to może zrobię odcinek 😅 Tu przyglądamy się tylko skutkom tego na konkretnym życiowym przykładzie.
A tak po ludzku:
Funkcja musi potrafić poradzić sobie z wszystkim co może do niej trafić jako argument. Moja funkcja potrafi opierdzielić dwa spośród trzech kolorów. Na niebieskim się nie zna.
Komponent ColorPicker natomiast chce aby onPickColor to była funkcja, do której może trafić także niebieski. Właściwie sam to zapisałem. Czy ma to sens? Spójrz na to:
const onPickColor = (color: Color | 'purple') => {};
Ta funkcja przechodzi, mimo że wydaje się, że nie powinna, bo zakres kolorów jest za szeroki i nie będzie wiadomo co zrobić z tym purple. Spójrzmy na to z dobrej strony. Moja funkcja potrafi obsłużyć wszystkie kolory i jeszcze purple.
Co widzi TS? Świetnie droga funkcjo masz znakomite kompetencje, aby stać się propsem dla onPickColor dla mojego komponentu ColorPicker. Potrafisz obsłużyć wszystkie kolory i a nawet więcej. I tak ich nie dostaniesz, ale miło z Twojej strony, że chcesz z nami współpracować.
Co jeszcze jest nadzbiorem moich kolorów? Jest to także string, bo wszystkie kolory są stringami. Zatem nawet takie coś przejdzie:
const onPickColor = (color: string) => {};
Natomiast próba podstawienia tam czegokolwiek innego lub brak argumentu będzie skutkować błędem:
const onPickColor = (color: number) => {};
const onPickColor = () => {};
Wiemy już jak interpretuje się typy w funkcjach. Wiemy dlaczego nasza konstrukcja jest potencjalnie niebezpieczna dla TypeScripta, dla JavaScripta też. W końcu ostatecznie będzie to skompilowane do JS. To ważne w kontekście tego co zaraz powiem.
Zupełnie inaczej interpretowalibyśmy całą sytuację, gdybym nie przekazywał kolorów w tablicy. Zestaw kolorów byłby zaszyty gdzieś indziej i wtedy już sami czujemy, że trzeba przekazać funkcję, która będzie się spodziewać przynajmniej zbioru mu zadeklarowanego, czyli nasz typ Colors.
Jak pozbyć się błędu szybko, niekoniecznie ładnie
Najszybciej ogarnąć to zmieniając funkcję na metodę.
interface ColorPickerProps {
colors: Array<Color>;
onPickColor(color: Color): void;
}
I właśnie w pokazałem jedną z różnic pomiędzy metodą a funkcją. Ta konkretna różnica jest ściśle związana z TypeScriptem. Ciekawostka: metody w TS są bivariantne. Kolejny trudny termin.
Zauważ, że JavaScriptowi nie robi to różnicy. Walić to! Jestem w JS, tu mogę wszystko 😎 Nie ma statycznego typowania, więc nie ma problemu z typowaniem.
Dlaczego TypeScript na to pozwala?
Twórcom i deweloperom TypeScripta bardzo zależy na tym, aby dało się nim otypować JavaScript. Doszli do wniosku, że muszą zostawić jakąś furtkę, którą będziemy mogli przepchnąć takie zachowanie.
Tutaj jednak trzeba uważać, bo ta funkcja może nie być w stanie poradzić sobie z zadaniem. Ustawiając typ jako metodę wpuszczamy gościa potencjalnie bez kompetencji. TypeScript mówi:
Hola hola koleżanko funkcjo! Do wykonania tego zadania jest potrzebny ktoś o szerszy kompetencjach, no chyba że jesteś metodą, to dostajesz specjalną przepustkę.
Ja wiem natomiast, że zbiór kolorów jest ograniczony przeze mnie samego i mogę zaufać tej funkcji.
Czy to nie jest droga na skróty?
Nie jest to rozwiązanie idealne, lepiej byłoby dynamicznie wnioskować typ tablicy i przekazywać jakoś do onPickColor. Jednak przyznam szczerze, że nie udało mi się tego zrobić.
Innym rozwiązaniem będzie, stworzenie specjalnej wersji ColorPickera tylko dla tego ograniczonego zbioru kolorów. W sumie to wystarczy podmiana typu props, implementacja mogłaby zostać.
Jeśli jednak wiesz co robisz, a po obejrzeniu tego wideo już wiesz, to użyj typu metody. To jak tabletka przeciwbólowa. Wiesz, że Cię nie wyleczy, ale przynajmniej na chwilę nie boli.
Z drugiej strony, jeżeli zawsze typujesz funkcje w props jako metody, bo wydaje Ci się, że nie ma różnicy, to masz potencjalną bombę. Bezpieczniejsze jest typowanie jako funkcja.