Dlaczego warto unikać prymitywów w TypeScript?
02/01/2023
3 min read
Prymitywy to podstawowe typy dostępne w TypeScript, takie jak
number
, string
lub boolean
. Używamy ich bardzo często definiując nasze zmienne, właściwości obiektów, pola klas itp. Chociaż są przypadki, w których primitywy mogą utrudnić nam życie i powinniśmy używać bardziej specyficznych typów.Ostatnio pracowałem nad funkcją śledzenia działań użytkownika w aplikacji i wysyłania metryk do Elastic APM. Ogólnie rzecz biorąc, powinno być śledzone, czy i które pola użytkownik dotykał i/lub zmieniał ich wartość. Aby rozróżnić pola, musieliśmy dodać jakiś parametr. Powiedzmy, że to
apmId
. Intuicyjnie można by założyć, że to id jest ciągiem znaków, więc komponent wyglądał mniej więcej tak:@Component({selector: 'form-first-field',template: `<form-field [apmId]="apmId"></form-field>`})export class FormFirstField {apmId: string = "firstField"}
Na pierwszy rzut oka wydaje się to ok. Jest komponent, który przekazuje
id
typu string do swojego dziecka. Nic ciekawego. Ale... co jeśli jest 10 lub więcej takich komponentów?Spróbuj na to spojrzeć inaczej. Przypadek jest taki, że chcemy znaleźć wszystkie komponenty, które są śledzone przez APM. Mając
apmId
jako string
nie jesteśmy w stanie ich szybko znaleźć. Nie ma żadnego związku między apmId
a jego celem. Ktoś mógłby powiedzieć: "dlaczego nie szukasz przez frazę apmId
?". Mogłbym, ale to pole może mieć inna nazwę w innych komponentach i to wyszukiwanie przez ciąg znaków nie jest ważne dla odniesienia. Inny scenariusz: musimy użyć apmId
w innym miejscu w kodzie. Jeśli nie pamiętamy wartości, musimy jej szukać w całej aplikacji. W tym miejscu jest bardzo łatwy sposób na popełnienie błędu, co prawdopodobnie pochłonie nas kilka godzin debugowania.Cóż... jaka jest rozwiązanie?
Stwórz własny typ! W moim przypadku był to typ, który zawiera nazwy wszystkich możliwych pól, które muszą być śledzone (nie musi to być koniecznie każde pole na formularzu). Więc mój typ wygląda mniej więcej tak:
type ApmTrackedField = 'firstField' | 'secondField' | 'someSpecialField' ...
I teraz użyjmy go w naszych komponentach:
//Component 1@Component({selector: 'form-first-field',template: `<form-field [apmId]="apmId"></form-field>`})export class FormFirstField {apmId: ApmTrackedField = "firstField"}//Component 2@Component({selector: 'form-secondField-field',template: `<form-field [apmId]="apmId"></form-field>`})export class FormsSecondField {apmId: ApmTrackedField = "secondField"}//other components
Jak widzisz, nasze "apmId" jest teraz bardzo specyficznym typem. Łatwo można go wyszukać w IDE, które komponenty mają coś wspólnego z funkcją śledzenia działań użytkownika. Jesteśmy bezpieczni na przyszłość, gdy nazwa pola się zmieni. TypeScript natychmiast rzuci błąd typu. Również w przypadku używania
apmId
w innym miejscu w kodzie, np. jako argumentu jakiejś metody, IDE podpowie możliwe wartości, więc unikniemy literówekPrzykład z życia pokazuje, jak ważne jest posiadanie odpowiednich typów. Jeśli mamy bardzo specyficzną domenę w projekcie, nie powinniśmy używać typów podstawowych, nawet jeśli wygląda na to, że tak:
type Price = number
Jest jasne, że
price
jest liczbą (lub ciągiem znaków), ale co jeśli pojawi się nowe wymaganie od biznesu, aby różnicować cenę według brutto/netto lub posiadać ją w USD i EURO. Dzięki temu typowaniu jesteśmy w stanie znaleźć wszystkie miejsca, które używają Price
i możemy oszacować, jak duże mogą być zmiany. Mając wszystkie ceny jako number
nie jesteśmy w stanie tego zrobić prosto.