Blog na którym chcę dzielić się wiedzą i doświadczeniem.

Typ any - praktyczne zastosowania

Marek Szkudelski
Marek Szkudelski
Podstawowy

Update: 26.02.2021

Opublikowałem artykuł, który obala stwierdzenia postawione w tym wpisie. Objaśniam w nim, dlaczego zastosowania typu any z tego artykułu są niepoprawne i przybliżam typ unknown. Unknown, czyli dlaczego nie stosować typy any

Update: 18.02.2021

Po dyskusji na grupie FB (JS news: after hours - link zadziała jeśli masz dostęp) muszę przyznać, że niestety nie mam racji w większości tego artykułu. Dlatego nie zachęcam Cię do czytania go. Wkrótce pojawi się nowy artykuł, w którym wyjaśnię, dlaczego ten praktycznie cały jest do wyrzucenia.

Wstęp

Niby wszyscy wiedzą o tym, czym jest typ any, ale jednak uważam, że wywołuje on sporo kontrowersji. Mniej doświadczeni programiści go nadużywają, a przez tych bardziej doświadczonych jest on najczęściej odradzany. Postanowiłem więc podsumować wszystkie znane mi przypadku użycia typu any w tym artykule.

Dlaczego raczej nie używać typu any?

Na ogół nie zaleca się nadmiernego używania tego typu ze względu na to, że TypeScript nie kontroluje co będziemy robić ze zmienną typu any. Pozwala na odwoływanie się do każdej właściwości oraz metody. Poprzez ten typ mówimy TypeScriptowi, że mamy do czynienia z czymkolwiek, więc może to być również dowolny obiekt z dowolnymi metodami. Jeśli wywołamy metodę, która nie istnieje, to dostaniemy błąd, ale nie podczas kompilacji, a w przeglądarce. Może to prowadzić do błędów w runtime’ie, przed którymi TS ma nas ustrzec. Jednak jest kilka praktycznych zastosowań tego typu.

Kolekcja czegokolwiek

Po pierwsze możemy użyć any jeśli spodziewany się dosłownie czegokolwiek. Na przykład tworzymy kolekcję przeróżnych wartości i nie za bardzo interesuje nas, co będzie w tej kolekcji.

Świetnym przykładem jest tutaj sam ReactJs, a dokładniej to, w jaki sposób ma otypowane zależności do hooków. Zobaczmy sobie na deklarację typu dla useEffect. deps jest tablicą tylko do odczytu i ta tablica może zawierać jakiekolwiek wartości.

function useEffect(
    effect: EffectCallback,
    deps?: DependencyList,
): void;
type DependencyList = ReadonlyArray<any>;

Dlaczego twórcy Reacta nie zawęzili tutaj typowania i pozwalają na przekazanie czegokolwiek? Aby odpowiedzieć na to pytanie, trzeba najpierw uświadomić sobie, do czego służą te zależności. React po każdym renderze komponentu porównuje nowe wartości przekazane w deps ze starymi (tymi, które byłe przekazane przy poprzednim renderze), żeby zobaczyć czy się zmieniły. Jeśli zmieniły się, to wykonuje się callback (tutaj parametr effect), a jeśli nie to nic się nie zadzieje.

Jeśli mamy porównać stare wartości z nowymi i interesuje nas tylko, to czy się zmieniły, to tak naprawdę nieważne, jaki mają typ. Operator porównania (=== lub ==) jest generyczny i uniwersalny dla każdego typu.

Nieznajomość kontraktu

Drugie zastosowanie typu any ma miejsce jeśli jeszcze nie znamy typu, a chcemy już zacząć implementować dany feature. Aby nie spowalniać developmentu, to możemy dodać typ any i zaimplementować funkcję, a potem zmienić typ i ewentualnie dostosować implementację do już znanego typu. Załóżmy, że implementujemy koszyk i chcemy utworzyć jego klasę, ale nie chcemy czekać, aż kontrakt API zostanie ustalony. Możemy zgadywać typy i struktury danych albo klasę otypować w taki sposób:

class Cart {
  constructor(private items: any[]) {}

  addItem(item: any) {}

  getItem(id: string): any {
    return this.items.find(
        (item) => item.id === id,
    );
  }
} 

Jednak kiedy przyjdzie moment, w którym poznamy kontrakt, może nam być ciężko, żeby podmienić typ any na odpowiedni model. Może się zdarzyć, że któreś miejsce pominiemy - szczególnie przy modelu używanym w wielu miejscach. Dlatego warto dodać sobie alias typu do miejsc, w których nie znamy dokładnej struktury, ale wiemy, że to będzie na pewno ten sam model.

type CartProduct = any;
 
class Cart {
  constructor(private items: CartProduct[]) {}

  addItem(item: CartProduct) {}

  getItem(id: string): CartProduct {
    return this.items.find(
        (item) => item.id === id,
    );
  }
} 

Co nam to daje? Kiedy zostanie ustalony kontrakt API, będziemy mogli skupić się na ewentualnych poprawkach implementacyjnych, a typ nadać tylko w jednym miejscu. Np. może okazać się, że pole id, nie nazywa się już tak, ale ma nazwę _id (wiem - skrajny przypadek). Jeśli mielibyśmy wszędzie typ any i zapomnielibyśmy podmienić ten typ na przykład w metodzie getItem, to mielibyśmy błąd w runtime'ie. Natomiast jeśli mamy alias typu, to tylko podmieniamy typ w jednym miejscu, a o błędnych założeniach poinformuje nas TypeScript.

Migracja z czystego JSa

Trzeci przypadek użycia, o którym warto wspomnieć, jest wtedy, kiedy migrujemy większy projekt z czystego JavaScriptu. W przypadku mniejszego projektu zazwyczaj jesteśmy w stanie zrobić to w ciągu najwyżej kilku godzin. Natomiast jeśli projekt jest większy, to możemy na początku napotkać trudności z dodaniem typów wszędzie na raz. A miejsca, w których nie dodamy typów, mogą powodować uruchomieniem projektu z błędami kompilacji. W tym przypadku możemy dodać any w miejscach, gdzie dostajemy błędy i potem stopniowo - już na działającym projekcie - dodawać konkretne typy. Takie rozwiązanie oczywiście powinno być tymczasowe.

Operacje na samych kluczach

Ostatnie zastosowanie, jakie znalazłem, to moment kiedy operujemy na kluczach obiektu. Kiedy chcemy otypować funkcję, która nam przemapuje klucze w jakiś sposób, to nie potrzebujemy znać typów wartości. To znaczy, potrzebujemy, ale tylko do typu zwracanego przez funkcję, a w jej ciele jest nam to zupełnie niepotrzebne.

function mapFields<
  T extends Record<string, any>,
>(object: T) {
    return object;
}

Popatrz na przykład powyżej. Jest bardzo uproszczony, ale chcę tylko nakreślić koncepcję. Określamy, że parametr object ma rozszerzać Record o kluczach, które są stringiem, a wartości mają być any.

Jeśli chcesz dowiedzieć się czym jest typ Record, to zapraszam Cię do mojego artykułu o Mapped Types.

Takie rozwiązanie jest poprawne, dopóki nie będziemy odwoływać się do wartości. Nie możemy na przykład wywołać ich jako funkcji albo odwołać się zagnieżdżonych właściwości. Jeśli potrzebujemy to zrobić, to musimy dokładniej określić kształt przekazywanego obiektu oraz jakie mogą być tam wartości. Jeśli byśmy tego nie zrobili, to mógłby wystąpić błąd, o którym pisałem we wstępie.

Mówiąc innymi słowy, określamy tylko kształt, jaki ma mieć nasz obiekt. Nie wskazujemy, czym dokładnie ma być, a jedynie definiujemy zarys. Możemy to zrobić, ponieważ w zasadzie nie obchodzi nas, co będzie w środku. Interesuje nas to, że kluczami będą stringi.

Update: 17.02.2021

Tak naprawdę ostatni przypadek użycia jest nie do końca poprawny. Owszem, chcemy tylko określić kształ obiektu, ale any może doprowadzić do błędów wewnątrz funkcji. Aby zapobiec takim błędom, powinniśmy użyć typu unknown dla wartości obiektu, zamiast any. Podziekowania dla Tomasza Ducina za tę uwagę!

Podsumowanie

Wydaję mi się, że przekazałem wam wszystkie poprawne przypadki użycia typu any, z którymi ja spotkałem się w pracy. Jeśli spotkaliście się w jakiejś innej sytuacji z poprawnym użyciem typu any, to możecie dać mi znać. Chętnie poznam inne podejścia do tego typu 🙂

Podsumujmy teraz jakie są zastosowania typu any w TypeScript. Można je tak naprawdę podzielić na dwie grupy:

  1. Kiedy nie interesuje nas zawartość, a jedynie ogólny kształt. Np. kiedy chcemy zbierać jakiekolwiek wartości, albo przekazać obiekt, którego tylko klucze są nam potrzebne.
  2. Kiedy tymczasowo chcemy ułatwić sobie pracę. Np. kiedy jeszcze nie znamy kontraktu API, albo migrujemy nasz projekt do TypeScriptu.

Artykuły, które też mogą Ci się spodobać:

W tym artykule obalam tezy, które postawiłem w poprzednim wpisie o typie `any`. Przybliżam typ `unknown` oraz to dlaczego jest lepszym rozwiązaniem przy typowanie niż `any`.

PodstawowyTypeScript

Uważam, że Mapped Types są często niedocenianym elementem TSa, więc w artykule pokazuję kilka z tych typów oraz wchodzę do środka, żeby pokazać jak można budować swoje własne typy.

ŚredniozaawansowanyTypeScript
ładowanie punktów reputacji...

Newsletter

Podobają Ci się treści, którę tworzę?

Zapisz się na newsletter.

Więcej info.