Poznajemy utility type TSa, jakim jest NoInfer. Ciekawe zagadnienie, a myślę, że w takiej konwencji shortów utility types łatwiej nam wejdą do głowy. Zaczynajmy.
Ok, rzućmy okiem na ten kod:
function createStreetLight<T extends string>(
colors: T[],
defaultColor?: T,
) {
// ...
}
createStreetLight(["red", "yellow", "green"], "red");
createStreetLight(["red", "yellow", "green"], "blue");
I teraz pomyślmy, jak TS domyśla się typu. Po pierwsze, ma zaznaczone, że ma to być typ napisowy. Dalej, pierwszy argument, pokazuje, że w colors ma być tablica tego typu. Drugi, opcjonalny argument (operator ?) pokazuje, że tam jest określony ten typ.
I TypeScript patrzy na typ, jaki został literałem napisu przekazany do funkcji. Jest to typ string, zarówno red jak i blue są typu string. I na tej podstawie TypeScript wnioskuje typ T.
Pamiętamy keyof? Fajna rzecz, ale służyła tylko do kluczy, my tutaj mamy z wartościami do czynienia. Ok, pamietamy literal types – robimy typowanie na kilka rodzajów konkretnych stringów. No, możemy:
type ValidColor = 'red' | 'yellow' | 'green';
function createStreetLight(
colors: ValidColor[],
defaultColor?: ValidColor,
) {
// ...
}
createStreetLight(["red", "yellow", "green"], "red"); //ok
createStreetLight(["red", "yellow", "green"], "blue"); //error
Mamy co chcieliśmy teoretycznie. Praktycznie to mamy te warości hard-coded, nie ma funkcji generycznej. Nam chodzi o to, aby TS domyślił się, że to drugie to pierwsze to ma być tablica napisów, a to drugie to ma być napis z tej tablicy.
I tu przychodzi nam z pomocą NoInfer:
function createStreetLight<T extends string>(
colors: T[],
defaultColor?: NoInfer<T>,
) {
// ...
}
createStreetLight(["red", "yellow", "green"], "red"); //ok
createStreetLight(["red", "yellow", "green"], "blue"); //error
Tylko teraz cała sztuka, to zrozumieć dlaczego. Bo że taki NoInfer wywala error dla blue to żadna sztuka zauważyć, edytor nam to pokazuje. Pytanie dlaczego.
Według słownika, słowo infer oznacza domyślać się, wnioskować. NoInfer tutaj pokazuje, że typ T ma nie być wnioskowany po argumencie przekazanym do defaultColor.
Czyli nie ma, że przekazano literał napisu, więc T to jest typ string. Ok, to z czego ma TS wnioskować? Z pierwszego argumentu. A jak wnioskuje?
Tutaj warto najechać myszką (zakładam że używamy VSCode, inaczej chyba tego bloga nie czyta ktoś, kto używa Vima, nie po to, aby się czegoś nauczyć) na czerwone podkreślenie blue i zobaczyć, co ten error nam mówi.
A mówi nam: Argument of type '”blue”’ is not assignable to parameter of type '”red” | „yellow” | „green” | undefined’
No tak, czyli TS po prostu zrobił union type wszystkich napisów w tablicy i to jest nasz T, bo po drugim argumencie ma zablokowane wnioskowanie. A skąd ten undefined?
Cóż, z domyślnego argumentu. Możemy zablokować opcjonalność tego argumentu:
function createStreetLight<T extends string>(
colors: T[],
defaultColor: NoInfer<T>,
) {
// ...
}
I proszę zobaczyć – wystarczy, że drugi argument jest czymś, co znajduje się w pierwszej tablicy, aby błędu nie było (nie ma to związku z usunięciem opcjonalności defaultColor, to po prostu teraz wymusi przekazanie drugiego argumentu):
createStreetLight(["red", "yellow", "green"], "red"); //ok
createStreetLight(["red", "blue", "green"], "blue"); //ok
NoInfer sprawiło, że TS przestał domyślać się typu po literale napisu z drugiego argumentu (a ten literał to jest typ string) a zaczął domyślać się po pierwszym argumencie, którym jest tablica typu T.
Generic nam mówi, że T ma dziedziczyć ze string, nic więcej. NoInfer nam mówi, że choć w drugim argumencie mamy T w czystej postaci, to TS ma nie brać tego i nie robić z tego wzoru T. Zatem TS bierze to, co ma, tablicę typu T, sprawdza, czy są napisowe (są) i robi z nich union type.
Dalej historię już znamy, koniec końców nie można tam przekazać jako defaultColor czegoś, czego w pierwszym argumencie nie ma.
Więcej TSa i JSa niedługo!