Wróć do strony głównej
Angular

Kompendium wiedzy o restrykcjach kompilacji Angularowego projektu

Jedną z głównych myśli przewodnich Typescripta jest to, by wyłapywać część błędów już na etapie samego pisania kodu (i/lub jego transpilacji). TS nie jest w tej kwestii zero-jedynkowy. W zależności od obranego podejścia może on być dość liberalny (w szczególności traktować każdy czysty JSowy kod jako prawidłowy TSowy kod), ale też, modyfikując w odpowiedni sposób konfigurację kompilatora, dostępne mamy szerokie spektrum poziomu restrykcyjności dla transpilowanego kodu. Podobnymi zasadami kieruje się również proces kompilacji Angularowych widoków (a właściwie szablonów widoków).

Motywacją do powstania niniejszego artykułu jest chęć zebrania informacji na temat wszystkich najważniejszych możliwych ustawień restrykcji kompilacji dla angularowego projektu w jednym miejscu. W szczególności dla każdej opcji z osobna przedstawiony będzie zwięzły opis restrykcji oraz przykładowy fragment kodu, na który dana restrykcja ma wpływ. Informacje zebrane poniżej są w ogólności dostępne w dokumentacjach Angulara i Typescripta, jednak w sporej części przypadków nie są one klarownie zaprezentowane i brak im przykładów (no i nie są też dostępne w języku polskim).

W obecnie najnowszej wersji Angulara (v12) strict mode stał się opcją domyślnie włączoną przy generowaniu nowego projektu za pomocą CLI (dla starszych wersji wymagana była dodatkowa flaga strict), więc to kolejny dobry powód, by pochylić się na chwilę nad dostępnymi dla nas opcjami.

W tym artykule pojęcia kompilacji i transpilacji będą stosowane zamiennie i oznaczać będą to samo (w kontekście omawianych treści).

Spis treści

  1. Czym jest Angular strict mode?
  2. Jaki jest cel Angular strict mode?
  3. Elementy składowe strict mode w Typescript’cie
    1. strictBindCallApply
    2. strictFunctionTypes
    3. strictNullChecks
    4. strictPropertyInitialization
    5. useUnknownInCatchVariables
  4. Dodatkowe restrykcje TS nie wchodzące w skład strict mode
    1. allowUnreachableCode
    2. allowUnusedLabels
    3. alwaysStrict
    4. exactOptionalPropertyTypes
    5. noFallthroughCasesInSwitch
    6. noImplicitAny
    7. noImplicitOverride
    8. noImplicitReturns
    9. noImplicitThis
    10. noPropertyAccessFromIndexSignature
    11. noUncheckedIndexedAccess
    12. noUnusedLocals
    13. noUnusedParameters
  5. Restrykcje dla kompilacji szablonów widoków Angularowych
    1. Tryb podstawowy (basic)
    2. Tryb pełnej weryfikacji (full mode)
    3. Tryb restrykcyjnej weryfikacji (strict mode)
    4. strictInputTypes
    5. strictInputAccessModifiers
    6. strictNullInputTypes
    7. strictAttributeTypes
    8. strictSafeNavigationTypes
    9. strictDomLocalRefTypes
    10. strictOutputEventTypes
    11. strictDomEventTypes
    12. strictContextGenerics
    13. strictLiteralTypes
  6. Podsumowanie
  7. Automatyzacja

Czym jest Angular strict mode?

Angular strict mode to tryb uruchamiający zaostrzone restrykcje w czasie developmentu aplikacji. Składają się na to:

  • strict mode w Typescript’cie (szczegóły), a więc włączenie szeregu dostępnych restrykcji dla kompilatora TSa,
  • włączenie szeregu restrykcji dla szablonów angularowych widoków (restrykcje dla Angular View Engine Compilator),
  • obniżenie wartości “bundle size budgets” w porównaniu do domyślnych ustawień Angulara.

Jaki jest cel Angular strict mode?

Kod spełniający dodatkowe restrykcje jest podatny na dokładniejszą statyczną analizę kodu (a więc jesteśmy w stanie wyłapać więcej błędów jeszcze w trakcie pisania kodu). W ogólności projekt staje się łatwiejszy do rozwoju i utrzymania. Ograniczamy też liczbę błędów, które pojawić mogłyby się dopiero w trakcie działania aplikacji. 
W dokumentacji Angulara znajdziemy również informację, że dla projektów rozwijanych w strict mode skorzystanie z ng update do automatycznego aktualizowania wersji frameworka jest bardziej precyzyjne i bezpieczniejsze.

Elementy składowe strict mode w Typescript’cie

Konfiguracja kompilatora języka Typescript zawiera opcję “strict”, której włączenie równoważne jest z włączeniem szeregu flag odpowiadających za dodanie dodatkowych restrykcji w trakcie transpilacji kodu. Możemy zarówno włączyć pełen strict mode, jak i uruchamiać tylko pojedyncze flagi (wedle potrzeby). W skład flag strict mode wchodzą:

strictBindCallApply

Włączenie tej flagi powoduje, że weryfikowana będzie zgodność typów argumentów przy użyciu wbudowanych w Javascript funkcji:

Przykład bez włączonej flagi (transpilacja przechodzi):

Rezultat z włączoną flagą (błąd transpilacji):

Rekomendacja: zawsze korzystać z flagi strictBindCallApply.

strictFunctionTypes

Dokumentacja dotycząca tej flagi jest mocno nieprecyzyjna i mówi jedynie o dokładniejszej weryfikacji typów argumentów funkcji. Dokładniejsze wyjaśnienie możemy znaleźć m.in. w książce “Typescript na poważnie” Michała Miszczyszyna, w której napisane zostało, że po włączeniu flagi argumenty funkcji nie mogą być biwariantne.

Czym są typy biwariantne? To również temat, który jest szeroko wyjaśniony we wspomnianej książce, a jego podsumowanie zawiera się w cytacie: 
Biwariancja: w miejscu typu X można użyć zarówno typu pochodnego, jak i nadrzędnego

Przykład bez włączonej flagi (transpilacja przechodzi):

Rezultat z włączoną flagą (błąd transpilacji):

Inny przykład (bez flagi):

Rezultat z włączoną flagą (błąd transpilacji):

Więcej informacji o typowaniu funkcji w Typescript’cie znajduje się tutaj. Więcej informacji o biwariancji (i wielu innych interesujących rzeczach) znajduje się w tej książce.

Rekomendacja: zawsze korzystać z flagi strictFunctionTypes.

strictNullChecks

Jest to prawdopodobnie jedna z flag wnoszących najwięcej zmian w pisanym na co dzień kodzie. Jak tłumaczy dokumentacja:

  • przy wyłączonej fladze typy null oraz undefined są ignorowane przez interpreter,
  • przy włączonej fladze typy null oraz undefined są rozróżnialne, dzięki czemu Typescript rozpozna wszystkie przypadki, w których te wartości mogą się pojawić.

Przykład bez włączonej flagi (kompilacja przechodzi):

Rezultat z włączoną flagą (błąd transpilacji dla wszystkich 3 zadeklarowanych stałych):

Innym korzystnym skutkiem ubocznym jest również wykrywanie wartości, które mogły nie zostać jeszcze zainicjalizowane. Przykład:

Rezultat z włączoną flagą (błąd transpilacji):

Innymi słowy flaga ta wprowadza wsparcie interpretera Typescripta dla wartości null’owalnych (ang. nullish, tj. mogące przyjąć wartość null lub undefined). Nowsze wersje języka zawierają dodatkową składnie, która służy właśnie do obchodzenia się z tego typu wartościami:

Więcej informacji o sposobach na obchodzenie się z wartościami null’owalnymi można znaleźć tutaj:

Więcej informacji o samym nullish znajduje się tutaj.

Rekomendacja: W razie możliwości (a w szczególności przy tworzeniu nowego projektu) gorąco zachęcamy do korzystania z tej flagi.

strictPropertyInitialization

Włączenie tej flagi wymaga uprzedniego włączenia “strictNullChecks”. W przeciwnym przypadku otrzymamy błąd:

Włączenie tej flagi stwarza konieczność inicjalizowania (przypisywania wartości) wszystkim atrybutom klasy w miejscu ich deklaracji lub w konstruktorze (nie jest możliwe inicjalizowanie tych wartości w metodzie wywołanej bezpośrednio w konstruktorze).

Spójrzmy na poniższy przykład (bez włączonej flagi):

Transpilacja takiego kodu odbędzie się pomyślnie. Zakładając, że za pomocą angularowego Input’a produkt jest przekazany do tego komponentu wszystkie console logi wykonają się poprawnie (nie nastąpi żaden błąd).

Jest to typowy przykład z życia Angularowego komponentu, gdzie wartości ustawiane za pomocą @Input, @ViewChild i tym podobnych nie są dostępne w momencie tworzenia instancji klasy komponentu, natomiast stają się dostępne na pewnym etapie życia komponentu (np. AfterViewInit dla @ViewChild). Więcej o cyklu życia Angularowego komponentu znajduje się tutaj.

Co się stanie, gdy włączymy flagę ’strictPropertyInitialization’?

Zgodnie z przypuszczeniami kod nie przechodzi pomyślnie przez proces kompilacji. W przypadku, w którym nie ma możliwości inicjalizacji wartości w momencie tworzenia instancji klasy istnieją dwa rozwiązania:

W przypadku opcji pierwszej (nullish)

powstaje konieczność obchodzenia się z tymi wartościami jak z nullowalnymi w każdym miejscu, w jakim z nich korzystamy (np. za pomocą optional chaining). Jest to opcja bezpieczna, ale zwiększająca potrzebny nakład pracy.

W przypadku opcji drugiej (non-null assertion)

programista bierze odpowiedzialność za to, że te wartości zostaną w odpowiednim momencie zainicjalizowane, a do tego czasu nie nastąpi próba odwołania się do ich wartości.

Rozwiązania te można połączyć, tj. typy ustawić na nullowalne, a w przypadku odwoływania się do wartości (i pewności, że są one już ustawione!) skorzystać z non-null assertion.

Rekomendacja: W przypadku skorzystania z tej flagi zalecamy stosowanie podejścia z nullowalnymi typami i unikanie korzystania z non-null assertion.

useUnknownInCatchVariables

Ta flaga dostępna jest w Typescripcie od wersji 4.4. W momencie pisania tego artykułu najnowsza wersja Angulara (12.1.1) nie wspiera jeszcze TS 4.4+.

Przed włączeniem tej flagi w bloku try-catch error był typowany jako ‘any’ i nie było możliwości zmiany tego (ponieważ sam JS pozwala rzucać dowolne wartości jako wyjątki).

Po włączeniu flagi ta sama zmienna ‘error’ otypowana jest jako ‘unknown’, przez co dostajemy błąd transpilacji:

Niezależnie od ustawienia flagi, od TS 4.4 error będziemy mogli otypować jawnie jako “any” lub “unknown”. Flaga wpływa wyłącznie na domyślny typ.

Więcej informacji o tym jak radzić sobie z typem unknown znajduje się tutaj.

Rekomendacja: Polecamy korzystać z tej flagi gdy tylko zaczniesz korzystać z Typescripta w wersji 4.4+.

Dodatkowe restrykcje TS nie wchodzące w skład strict mode

Konfiguracja kompilatora języka Typescript zawiera też szereg innych interesujących reguł implementujących dodatkowe restrykcje:

allowUnreachableCode

Jest to jedna z flag, która dla wartości false, nakłada większe restrykcje, niż przy wartości true. W zależności od wartości tej flagi:

  • dla wartości true nadmiarowy kod, który nigdy nie zostanie wykonany, jest ignorowany,
  • dla undefined (wartość domyślna) interpreter Typescripta w taki sam sposób (jak w przypadku true) ignoruje ten kod, ale zapewnia wsparcie przy wyświetlaniu warningów w edytorze kodu. Popularne IDE generują warningi również przy fladze ustawionej na true,
  • dla wartości false kod, który nigdy nie zostanie wykonany powoduje błąd kompilacji.

Przykład (z flagą ustawioną na true kompilacja się powiedzie):

Z flagą ustawioną na false:

Rekomendacja: Flagę ustawić na false, o ile nie mamy do czynienia z legacy kodem, przy którym jest to kłopotliwe.

allowUnusedLabels

Etykiety (labels) to rzadko spotykany i używany element składni Javascriptu (a więc i Typescriptu) współpracujący z poleceniami break i continue, pozwalający identyfikować pętle za pomocą nadanych im nazw (etykiet) i przerywać/kontynuować ich wykonywanie (odwołując się do konkretnej pętli, nawet w przypadku zagnieżdżenia pętli w pętli).

Javascript/Typescript pozwala nam zdefiniować etykietę w niemalże dowolnym miejscu (co oczywiście z reguły nie ma sensu i powinno być automatycznie uznane za błąd):

Przy włączonej restrykcji (wartość flagi ustawiona na false) przy każdej nadmiarowo zdefiniowanej etykiecie otrzymujemy błąd:

Ciekawostka: wracając do powyższego przykładu funkcji isInMatrix  – etykieta (loopOverY) jest opcjonalna, poniewaz jej pominięcie przy komendach break/continue i tak skutkować będzie przerwaniem/kontynuowaniem wykonywania najbardziej zagnieżdżonej pętli. W tym jednak przypadku Typescript pozwala zachować nam tę etykietę (co naszym zdaniem poprawia czytelność, gdy już decydujemy się skorzystać z etykiet w zagnieżdżonych pętlach.

Rekomendacja: Flagę ustawić na false. Z etykiet korzystać wyłącznie w przypadku zagnieżdżonych pętli i potrzeby przerywania/kontynuowania wykonywania ich kolejnych  iteracji.

alwaysStrict

Ta flaga nie ma bezpośredniego wpływu na kod pisany w Typescripcie, natomiast sprawia ona, że wszystkie pliki kompilowane są do Javascriptu z wykorzystaniem Ecmascript strict mode. Sam Ecmascript strict mode jest materiałem na osobny artykuł, jednak w wielkim skrócie:

  • do każdego pliku *.js dodawany jest prefix “use strict”
  • większość silników Javascriptowych (tj. wszystkie kompatybilne z tym trybem) interpretują kod JS w sposób bardziej restrykcyjny (w trakcie runtime), m.in. część błędów, które w trybie “normalnym” zostałyby zignorowane w tym przypadku zostają rzucone

exactOptionalPropertyTypes

Ta flaga dostępna jest w Typescripcie od wersji 4.4. W momencie pisania tego artykułu najnowsza wersja Angulara (12.1.1) nie wspiera jeszcze TS 4.4+.

Aby wyjaśnić zasadność tej flagi nakreślmy pewien kontekst:

Obiekt ustawień aplikacji (typu ApplicationSettings) posiada pole theme, które może przyjmować wartości: ‘Dark’, ‘Light’ lub undefined. Na dwa różne sposoby definiujemy obiekty settingsA i settingsB.  W pierwszym przypadku pomijamy klucz ‘theme’, natomiast w drugim jawnie nadajemy mu wartość undefined. W znakomitej większości przypadków pole theme obu obiektów będzie interpretowane w ten sam sposób:

Są jednak przypadki, w których oba obiekty będą interpretowane w odmienny sposób:

Sam console.log w jasny sposób pokazuje nam różnice:

W celu uniknięcia tego typu rozbieżności w Typescripcie 4.4 wprowadzono flagę 'exactOptionalPropertyTypes’. Jej zadaniem jest uniemożliwienie jawnego definiowania pól opcjonalnych wartością undefined (tak aby w przypadku braku wartości w polu opcjonalnym w obiekcie wynikowym nie było zdefiniowanego klucza dla tej wartości).

Przed włączeniem flagi (kompilacja przechodzi pomyślnie):

Po włączeniu flagi (mimo wciąż opcjonalnego pola theme):

Rekomendacja: Polecamy korzystać z tej flagi gdy tylko zaczniesz korzystać z Typescripta w wersji 4.4+.

noFallthroughCasesInSwitch

Włączenie tej flagi powoduje, że każdy przypadek (case) w switch/case, który posiada jakiekolwiek instrukcje do wykonania (grupowanie kilku przypadków jeden po drugim jest nadal dopuszczalne) musi kończyć się słowem kluczowym break lub return (w przypadku, gdy switch/case znajduje się wewnątrz funkcji).

Przykład bez włączonej flagi (kompilacja przechodzi):

W przypadku włączenia flagi 'noFallthroughCasesInSwitch’ otrzymujemy następujący błąd:

Flaga ta ma pomagać uniknąć przypadkowego pominięcia słów kluczowych break i/lub return.

Rekomendacja: Włączyć flagę noFallthroughCasesInSwitch.

noImplicitAny

W języku Typescript, gdy nie zdefiniujemy typu jawnie, interpreter usiłuje wywnioskować go z kontekstu. W przypadku, w którym nie uda się w żaden sposób zawęzić możliwego typu przyjmuje on wartość any.

Włączona flaga noImplicitAny powoduje, że niemożność wywnioskowania typu z kontekstu i brak jawnego zdefiniowania typu przez programistę skutkuje błędem kompilacji. 

Przykład bez włączonej flagi:

Włączenie flagi powoduje w tym miejscu błąd:

Rekomendacja: Zalecamy korzystać z tej flagi w każdym projekcie. Należy zwrócić uwagę na konieczność zdefiniowania typów dla bibliotek zewnętrznych, które nie posiadają typowania.

noImplicitOverride

Jest to flaga, która wraz z nowym słowem kluczowym override pojawiła się w Typescript’cie 4.3+. W przypadku jej włączenia każde nadpisanie metody lub pola w klasie dziedziczącej musi zostać jawnie poprzedzone słówkiem override. Dzięki temu możemy uniknąć sytuacji, w której np. zmienimy nazwę metody w klasie macierzystej bez zmiany nazwy w klasach po niej dziedziczących.

Przykład bez włączonej flagi:

Błąd po włączeniu flagi:

Rekomendacja: Włączyć flagę i zawsze korzystać ze słówka override.

noImplicitReturns

Po włączeniu tej flagi dla każdej funkcji weryfikowane są wszystkie możliwe ścieżki kodu. Jeśli któraś ze ścieżek nie zwraca zadeklarowanego typu (lub w przypadku braku zadeklarowania zwracanego typu część ścieżek zwraca “coś”, a część nie) rzucany jest błąd.

Przykład bez włączonej flagi:

Po włączeniu flagi dla każdej z powyższych funkcji otrzymy błąd:

Taki sam błąd powstaje w przypadku, w którym nie definiujemy jawnie zwracanego typu i pozwalamy zrobić to Typescriptowi, ale jednocześnie nie wszystkie ścieżki zwracają jakąkolwiek wartość.

Rekomendacja: Zawsze korzystać z flagi noImplicitReturns.

noImplicitThis

Przy włączonej fladze błąd zostaje rzucony, gdy nie zdefiniujemy jawnie typu dla “this”, a Typescript nie jest w stanie wywnioskować jego typu z kontekstu.

Rzućmy okiem na błędny przykład:

Mamy tutaj metodę, która zawiera w sobie zdefiniowaną nową funkcję (klasyczną, nie “arrow-function”, a więc wartość “this” zmienia się w zależności od kontekstu/sposobu wywołania tej funkcji).

Po włączeniu flagi słusznie otrzymamy błąd:

Dla przypomnienia parametr “this” możemy typować jawnie, dzięki czemu np. otypowane w taki sposób funkcje można wywołać tylko w konkretnym kontekście. Dorzućmy flagę “strictBindCallApply”, która pozwoli nam swobodnie podmieniać typy wraz z restrykcyjną weryfikacją tychże.

Rekomendacja: Zawsze korzystać z flagi noImplicitThis.

noPropertyAccessFromIndexSignature

Jeżeli część pól otypujemy za pomocą “index signature” to wówczas, bez włączonej flagi, możemy odwoływać się za pomocą kropki (np. “foo.bar”) do dowolnych pól, nawet, jeśli nie są one zdefiniowane:

Po włączeniu flagi “noPropertyAccessFromIndexSignature” do atrybutów zdefiniowanych za pomocą “index signature” możemy uzyskać dostęp wyłącznie za pomocą “index signature”. Dzięki temu za pomocą “kropki” nigdy nie odwołamy się do pól, które mogą być niezdefiniowane.

Błędy kompilacji po włączeniu flagi:

Rekomendacja: Zawsze korzystać z flagi noPropertyAccessFromIndexSignature.

noUncheckedIndexedAccess

Ta flaga współpracuje w połączeniu z flagą “strictNullChecks” i powoduje, że do pól, które otypowane są za pomocą “index signature” jako typ “X” zwracany jest typ “X | undefined”.

Błąd kompilacji po włączeniu flagi:

Rekomendacja: Zawsze korzystać z flagi noUncheckedIndexedAccess.

noUnusedLocals

Zasada działania jest dość prosta – nieużyte zadeklarowane zmienne lokalne wywołują błąd. Warto zwrócić uwagę, że tyczy się to również nieużytych zaimportowanych do pliku modułów.

Przykład pliku z nieużytymi zadeklarowanymi zmiennymi:

Błędy kompilacji po włączeniu flagi:

Rekomendacja: Zachęcamy do spróbowania. Bardzo przydatne okazują się narzędzia do usuwania nieużytych importów (np. domyślnie Ctrl+Alt+O w Webstormie).

noUnusedParameters

Podobnie jak w przypadku “noUnusedLocals” niedozwolone stają się zadeklarowane, ale nieużyte argumenty funkcji.

Przykład funkcji z nieużytym argumentem:

Błąd kompilacji po włączeniu flagi:

Rekomendacja: Zawsze korzystać z tej flagi, ponieważ pozbywamy się zbędnych argumentów, co m.in. w wyraźny sposób poprawia czytelność kodu.

Restrykcje dla kompilacji szablonów widoków Angularowych

Zacznijmy od tego, że w ramach opcji kompilatora szablonów widoków Angularowych dostępne są aż 3 tryby weryfikacji typów zmiennych użytych w szablonach. Prezentują się one następująco:

Tryb podstawowy (basic):

w takim trybie pracujemy przy następującym ustawieniu flag (fragment tsconfig.json):

Odwołując się do zmiennych sprawdzane jest jedynie to, czy te zmienne istnieją (są polami klasy komponentu) oraz czy posiadają pola, do których się odwołujemy. Przykładowo:

Weryfikowane są następujące rzeczy:

  • czy “user” jest polem w klasie komponentu,
  • czy “user” jest obiektem z polem “address”,
  • czy “address” jest obiektem z polem “city”.

Nie weryfikowane jest czy typ “user.address.city” jest zgodny z typem inputa “street” w komponencie “app-child” (nie jest). Kompilacja się powiedzie, ale w trakcie runtime wystąpi błąd:

Rzeczy, które dodatkowo nie są weryfikowane na etapie kompilacji w tym trybie:

  • zmienne w widokach “embedded” (np. zmienne użyte w *ngIf, *ngFor, <ng-template> mają zawsze typ “any”). Poniższy przykład przechodzi przez proces kompilacji, zmienne “fruit” oraz “user” mają typ “any”.

  • typy referencji (#refs), wartości zwracanych przez pipes, typy wartości $event emitowane przez wszelkie outputy mają zawsze typ “any”.

Tryb pełnej weryfikacji (full mode):

w takim trybie pracujemy przy następującym ustawieniu flag (fragment tsconfig.json):

W porównaniu do trybu podstawowego zmieniają się następujące rzeczy:

  • zmienne w widokach “embedded” (np. zmienne wewnątrz bloków *ngIf, *ngFor, <ng-template>) mają poprawnie wykrywany i weryfikowany typ,
  • wykrywany i weryfikowany jest typ wartości zwracanych z pipes,
  • lokalne referencje (#refs) do dyrektyw i pipes mają poprawnie wykrywany i weryfikowany typ (z wyjątkiem, gdy parametry te są generyczne).

W poniższym przykładzie zmienna lokalna “fruit” jest wciąż typu “any”, ale “user” ma już poprawnie wywnioskowany typ.

W tym trybie w trakcie kompilacji pojawi się błąd:

Tryb restrykcyjnej weryfikacji (strict mode):

w takim trybie pracujemy przy następującym ustawieniu flag (fragment tsconfig.json):

Ustawienie flagi “strictTemplates” na “true” zawsze nadpisuje wartość “fullTemplateTypeCheck” (tak więc flagę “fullTemplateTypeCheck” można w takim przypadku pominąć).

W tym trybie kompilator oferuje nam wszystko to, co w trybie pełnej weryfikacji, a ponadto:

  • dla komponentów i dyrektyw weryfikowane są zgodności typów inputów z przypisywanymi do nich zmiennymi w szablonie (wspomniana w sekcji o Typescript’cie flaga strictNullChecks jest również brana przy tej weryfikacji pod uwagę),
  • wnioskuje typy dla zmiennych lokalnych wewnątrz widoków “embedded” (np. zmienna lokalna zadeklarowana wewnątrz dyrektywy strukturalnej *ngFor),
  • wnioskuje typ wartości $event dla outputów z komponentów, eventów DOM oraz angularowych animacji,
  • wnioskuje typ referencji (#refs) również dla elementów DOM na podstawie nazwy tagu (np. <span #spanRef> zostanie poprawnie otypowane jako HTMLSpanElement).

Przy włączonej dodatkowo fladze strictNullChecks otrzymamy następujący błąd dotyczący przypisania wartości “applicationName” do inputa “name”:

W tym trybie również lokalna zmienna “fruit” będzie mieć poprawnie wywnioskowany typ (string), a więc:

Przy kombinacji flag strictNullChecks oraz strictTemplates warto też zwrócić uwagę na wykorzystywany często async pipe. Typ metody “transform” tego pipe’a otypowany jest następująco (z wykorzystaniem overload):

Oznacza to, że dla następującego kodu:

otrzymamy błąd kompilacji:

gdyż wyrażenie “applicationName$ | async” zwraca nam wartość typu “string | null”. Oznacza to konieczność typowania wszystkich inputów (do których wartość przypisywana jest za pomocą async pipe) jako nullable.

Uwaga: wpływ flagi strictNullChecks na weryfikację typów inputów można ustawić za pomocą flagi strictNullInputTypes, o której mowa będzie w dalszej części artykułu.

Rekomendacja: Stanowczo odradzamy korzystanie z trybu basic, mocno zachęcamy do korzystania z trybu scrict, który pozwoli uniknąć wielu błędów.

strictInputTypes

Jest to flaga odpowiadająca za weryfikację typów inputów. Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”. 

Przy wartości “true” weryfikowane są typy zmiennych przypisywanych do inputów, tak jak pokazano to w przykładzie dla trybu restrykcyjnej weryfikacji, a dla wartości false ta weryfikacja jest całkowicie pomijana (nawet przy równocześnie włączonej fladze strictTemplates).

Dla przypomnienia – restrykcyjność tej weryfikacji (tj. branie pod uwagę wartości nullable) zależy również od wartości flagi strictNullChecks (oraz flagi strictNullInputTypes, o której mowa będzie w dalszej części artykułu).

Rekomendacja: Zawsze korzystać z tej weryfikacji (ustawiając bezpośrednio tę flagę na true, lub za pośrednictwem flagi strictTemplates).

strictInputAccessModifiers

Ta flaga jest odpowiedzialna za weryfikację modyfikatorów dostępu do pól (private/protected/readonly) przy przypisywaniu zmiennych do inputów.

Dla powyższego przykładu, bez włączonej flagi strictInputAccessModifiers kompilacja się powiedzie (w trakcie runtime nie wystąpi żaden błąd). Dla włączonej flagi otrzymamy odpowiednio:

Nałożenie dekoratora @Input na pole z modyfikatorami dostępu readonly/private/protected wydaje się programistycznym błędem w każdym przypadku (gdyż umożliwia to modyfikację tych wartości z zewnątrz, w dowolnej chwili, na co żaden z tych modyfikatorów teoretycznie nie zezwala), jednak dopiero ta dodatkowa flaga zapobiega tego typu błędom.

Rekomendacja: Ta flaga nie zawiera się w strictTemplates, dlatego należy włączyć ją osobno! Zalecamy korzystać z niej (i nie nakładać wspomnianych modyfikatorów dostępu na pola oznaczone dekoratorem @Input).

strictNullInputTypes

Ta flaga decyduje o tym, czy flaga strictNullChecks jest brana pod uwagę przy weryfikacji typów zmiennych przypisywanych do Inputów. Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”.

Wróćmy ponownie do przykładu z async pipe:

Dla włączonej flagi “strictNullChecks” i domyślnych wartości flag “strictInputTypes” oraz “strictNullInputTypes” (które można by pominąć, a wówczas ich wartość wynikałaby z wartości “strictTemplates”):

otrzymamy przytoczony już wcześniej błąd (ponieważ zwracany typ z async pipe jest nullable):

Jeśli jednak ustawimy tę flagę na “false” przy jednocześnie włączonych flagach “strictNullChecks”, “strictTemplates” oraz “strictInputTypes”:

to wówczas kompilacja się powiedzie (ponieważ string pasuje do string’a, a nullowalność jest ignorowana).

Rekomendacja: Zalecamy nie wyłączać tej flagi, chociaż w przypadku już istniejących projektów (w którym inputy nie są nullowalne) oraz korzystania z bibliotek, w których komponenty nie są napisane z myślą o wspieraniu wartości nullowalnych może okazać się to konieczne.

strictAttributeTypes

Jest to flaga odpowiadająca za weryfikację przypisywania wartości do inputów za pomocą “text attributes” (zamiast klasycznego bindingu). Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”.

Zwykle dokonujemy wiązania atrybutów (w tym inputów dla komponentów i dyrektyw) za pomocą kwadratowych nawiasów “[]” (informując tym samym kompilator angulara, że wyrażenie po prawej stronie jest ‘dynamiczne’, a więc zawiera w sobie jakieś wyrażenie, które wymaga obliczenia (może być to w najprostszym przypadku referencja na jakąś zmienną).

Istnieje też drugi sposób na ustawienie wartości inputa – jako zwykły HTMLowy atrybut (pamiętając, że wszystkie takie atrybuty są stringami). Jeśli nazwa atrybutu pokrywa się z nazwą inputa to wartość inputa jest odpowiednio ustawiana.

Przy wyłączonej fladze strictAttributeTypes poniższy przykład przejdzie kompilacje:

doprowadzi to do ustawienia wartości “weight” jako “18” (string, nie number), co w efekcie doprowadzi do runtime exception:

Przy włączonej fladze kompilator wyłapie taki błąd:

Przypisywania wartości do inputów z pominięciem kwadratowych nawiasów możemy używać wyłącznie dla Inputów otypowanych jako string (i enum, którego wartości są również stringami).

Rekomendacja: Korzystać z tej flagi (przy korzystaniu ze strictTemplates nie wyłączać jej).

strictSafeNavigationTypes

Czym są operacje “safe navigations”? Jest to dobrze nam znany odpowiednik Optional Chaining z Typescripta, ale występujący po stronie angularowego szablonu. Przykładowo:

Przy wyłączonej fladze każde użycie safe navigation operatora spowoduje, że jego rezultat będzie traktowany jako “any”. Przy włączonej fladze typ będzie prawidłowo wnioskowany. Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”.

Bez włączonej flagi safe navigation operator weryfikuje jedynie, czy “address” jest polem w obiekcie “user”, ale w dalszej kolejności traktuje tę wartość jako “any”, tak więc możliwe jest odwołanie się do nieistniejących pól (“bar.baz”). 

O błędzie dowiemy się dopiero w trakcie runtime (“Cannot read property 'baz’ of undefined”). Jeśli flaga jest włączona, wówczas wartość zwracana przez operator safe navigation jest poprawnie wnioskowana, a błąd zostanie wykryty już na etapie kompilacji:

Rekomendacja: Korzystać z tej flagi (przy korzystaniu ze strictTemplates nie wyłączać jej).

strictDomLocalRefTypes

Ta flaga odpowiada za wyłączenie/włączenie wnioskowania typów angularowych referencji nałożonych na elementy DOM. Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”. Z naszych testów wynika również, że bez włączonej flagi “strictTemplates” typy referencji nie są wnioskowane niezależnie od ustawienia flagi strictDomLocalRefTypes.

Przykład z wyłączoną flagą strictDomLocalRefTypes (przy włączonej fladze strictTemplates):

Kompilacja przechodzi, błąd pojawia się dopiero w runtime:

Przy włączonych obu flagach (wywnioskowany typ referencji dla tagu “input” to “HTMLInputElement”) następuje błąd kompilacji:

Rekomendacja: Korzystać z tej flagi (przy korzystaniu ze strictTemplates nie wyłączać jej).

strictOutputEventTypes

Ta flaga odpowiada za wyłączenie/włączenie wnioskowania typów dla wartości $event występującej przy outputach z komponentów/dyrektyw oraz angularowych animacjach. Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”.

Przykład z wyłączoną flagą:

Kompilacja przechodzi, błąd pojawia się dopiero w runtime, po pierwszym wyemitowaniu eventu: 

Przy włączonej fladze typ $event jest poprawnie wywnioskowany (jako “number”) i następuje błąd kompilacji:

Rekomendacja: Korzystać z tej flagi (przy korzystaniu ze strictTemplates nie wyłączać jej).

strictDomEventTypes

Ta flaga, podobnie jak flaga strictOutputEventTypes, odpowiada za wnioskowanie typu $event, ale tym razem dla natywnych eventów z DOM. Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”.

Przykład z wyłączoną flagą:

 

Kompilacja przechodzi, błąd pojawia się dopiero w runtime, po pierwszym wyemitowaniu eventu:

Przy włączonej fladze typ $event jest poprawnie wywnioskowany (jako “MouseEvent”) i następuje błąd kompilacji:

Rekomendacja: Korzystać z tej flagi (przy korzystaniu ze strictTemplates nie wyłączać jej).

strictContextGenerics

Ta flaga dotyczy typów generycznych dla komponentów. Jeżeli jest wyłączona, wówczas w trakcie wnioskowania typów w angularowym szablonie każde wystąpienie typu generycznego komponentu jest interpretowane jako “any”. Przy włączonej fladze generyczne typy komponentu są poprawnie uwzględniane przy wnioskowaniu typów. Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”.

Rozważmy następujący przykład:

Zmienna “value” jest typu “T”, a więc wiemy na pewno, że jest tablicą jakichś elementów. Przy wyłączonej fladze “value” jest jednak interpretowane po stronie szablonu jako “any”, przez co możemy łatwo doprowadzić do runtime exception.

Przy włączonej fladze otrzymamy słuszny błąd kompilacji:

Kompilator nie ma zastrzeżeń do skorzystania z pola “length’, gdyż każda tablica posiada te pole.

Rekomendacja: Korzystać z tej flagi (przy korzystaniu ze strictTemplates nie wyłączać jej).

strictLiteralTypes

Ta flaga decyduje o tym, czy zmienne (konkretnie obiekty i tablice), które deklarujemy bezpośrednio w szablonie mają wnioskowany typ (w przypadku wyłączenia flagi ich wartość to “any”). Jeśli nie ustawimy tej flagi ręcznie, to jej domyślna wartość odpowiada wartości flagi “strictTemplates”.

Przykład z wyłączoną flagą:

W szablonie deklarujemy dwie zmienne (obiekt z polem ‘firstName’ oraz tablicę z dwoma stringami). Oba postrzegane są jako “any”, więc możemy odnosić się do nieistniejących pól (co skutkować będzie błędami w runtime).

Przy włączonej fladze otrzymamy następujące błędy kompilacji:

Rekomendacja: Korzystać z tej flagi (przy korzyrstaniu ze strictTemplates nie wyłączać jej).

Podsumowanie

Brawo! Przebrnęliśmy przez długą listę możliwych do ustawienia restrykcji. Teraz dokładnie wiemy co i w jaki sposób możemy ustawić, aby dopasować proces kompilacji do własnych potrzeb. Każda z flag została (celowo) przedstawiona w sposób możliwie niezależny od pozostałych. To nie zmienia jednak faktu, że wszystkie te restrykcje współistnieją, częściowo wpływają na siebie i się wzajemnie uzupełniają.

Znalezienie idealnych ustawień dla Twojego projektu (czy zespołu, bo ostatecznie sama konfiguracja jest reużywalna) może być procesem pełnym prób i eksperymentów. W ogólności sugeruję kierować się zawsze w stronę bardziej restrykcyjnych konfiguracji.

Automatyzacja

Aby “uszczelnić” proces weryfikacji kodu (w tym weryfikacji możliwości kompilacji) zachęcamy do włączenia do procesu ciągłej integracji etapu próbnej kompilacji projektu, aby faktycznie (tak jak wspomniano to na samym początku artykułu) jak najszybciej wyłapać wszelkie błędy.

Restrykcyjne zasady kompilacji stanowią dobre uzupełnienie pozostałych technik weryfikacji poprawności kodu (takich jak zaawansowana statyczna analiza kodu czy wszelkiego rodzaju testy automatyczne). Każdy błąd wyłapany przez automatyczny mechanizm stanowi ostatecznie sporą oszczędność czasu i pieniędzy (w porównaniu do znalezienia tych samych błędów w trakcie testów manualnych, lub co gorsza po udostępnieniu aplikacji użytkownikom końcowym).

 

O autorze

Mateusz Dobrowolski

Sympatyk Typescripta mający kilkuletnie doświadczenie w tworzeniu Angularowych aplikacji.

Chcesz razem z nami tworzyć treści na bloga? Dołącz do nas i twórz wartościowe treści dla sympatyków Angulara z Angular.love!

0 komentarzy

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *