Menu

Wykrzyknik w TypeScript: non-null assertion (!)

Wykrzyknik po wartości, na przykład user!, to operator non-null assertion: usuwa null i undefined z typu bez żadnego sprawdzenia w czasie działania. Zobacz, co robi x!, jak działają formy definite assignment let x!: T i prop!: T, dlaczego są ryzykowne i jakie są bezpieczniejsze alternatywy.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Wykrzyknik po wyrażeniu, value!, to operator non-null assertion. Usuwa null i undefined z typu, więc number | undefined można użyć jako number. To obietnica dana kompilatorowi, a nie sprawdzenie: w czasie działania nic się nie dzieje.

Bez ! wywołanie tea.toFixed(2) to błąd TS18048, 'tea' is possibly 'undefined'. Z nim kod się kompiluje, bo zapewniasz kompilator, że klucz istnieje.

Co x! robi w czasie działania: nic

! jest usuwany z wyniku kompilacji. Skompilowany JavaScript dla prices.get("coffee")! to po prostu prices.get("coffee"). Jeśli asercja jest błędna, błąd pojawia się później, w pierwszym miejscu, gdzie brakująca wartość zostaje użyta:

Program wypisuje Cannot read properties of undefined (reading 'toFixed'). Awaria następuje w linii po !, a w prawdziwym kodzie może być znacznie dalej: undefined może trafić do obiektu i wybuchnąć w zupełnie innym pliku. Ta odległość sprawia, że ! jest ryzykowny.

Definite assignment: let x!: T

Ten sam symbol w deklaracji oznacza coś pokrewnego. TypeScript śledzi, czy zmienna została przypisana przed odczytem, ale nie potrafi prześledzić przypisania wykonanego w innej funkcji:

index.ts(9,13): error TS2454: Variable 'config' is used before being assigned.

let config!: { port: number }; to definite assignment assertion: "zostanie przypisana przed każdym odczytem". Usuwa błąd, z tym samym haczykiem co x!: jeśli init() kiedyś zostanie pominięte, odczyt dostanie undefined. Zwykle lepiej zmienić strukturę kodu, na przykład const config = init();, gdzie init zwraca obiekt.

Właściwości klas: prop!: T

Przy strictPropertyInitialization (część strict) każda właściwość klasy musi być zainicjalizowana w deklaracji lub w konstruktorze. W przeciwnym razie dostajesz TS2564: Property 'socket' has no initializer and is not definitely assigned in the constructor. Gdy właściwość ustawia później metoda lub framework, prop!: T każe kompilatorowi ją zaakceptować:

Pierwsze console.log pokazuje lukę: typ mówi, że socket zawsze istnieje, ale przed open() jest undefined. Wywołanie c.socket.send w tym momencie się kompiluje i rzuca wyjątek. Jeśli właściwości naprawdę może brakować, zadeklaruj ją jako socket?: ... i sprawdzaj, albo twórz obiekt w konstruktorze. Główne miejsce, gdzie prop! to standardowa praktyka, to framework wypełniający właściwość po utworzeniu obiektu: @ViewChild(...) child!: ChildDirective w Angularze (ustawiane przed uruchomieniem ngAfterViewInit) albo klasy encji ORM, których kolumny biblioteka wypełnia przy wczytywaniu wiersza (dokumentacja MikroORM pisze @Property() title!: string).

Bezpieczniejsze alternatywy

Większość ! da się zastąpić czymś, co kompilator sprawdza, albo sprawdzeniem, które głośno zawodzi we właściwym miejscu:

Obie funkcje pomocnicze zawężają typ tak jak !, ale błędne założenie od razu daje missing HOST zamiast TypeError gdzieś indziej. assertDefined to funkcja asercji (asserts value is ...): po wywołaniu kompilator traktuje host jako string. Więcej wzorców w type guards.

ZamiastNapiszCo się dzieje, gdy brakuje wartości
user!.nameif (user) { user.name }blok zostaje pominięty
user!.nameuser?.nameundefined
count!count ?? 0używana jest wartość domyślna
map.get(k)!must(map.get(k), "k")czytelny błąd w tej linii
let x!: Tconst x = compute()nie ma co pójść źle

Inne wykrzykniki

! znaczy różne rzeczy w zależności od tego, gdzie stoi:

KodZnaczenie
value! (po wyrażeniu)non-null assertion, tylko TypeScript
let x!: T, prop!: Tdefinite assignment assertion, tylko TypeScript
!value (przed wyrażeniem)logiczne NOT, zwykły JavaScript
!!valuekonwersja na boolean, zwykły JavaScript
a !== b, a != bnierówność, zwykły JavaScript

Tylko dwa pierwsze są usuwane podczas kompilacji. !value i !!value działają w czasie wykonania i zwracają boolean.

Najczęściej zadawane pytania

Co oznacza wykrzyknik po zmiennej w TypeScript?

value! to operator non-null assertion. Mówi kompilatorowi, że value nie jest null ani undefined, więc jego typ traci te dwa elementy: string | undefined staje się string. Znika z wygenerowanego JavaScriptu i nie dodaje żadnego sprawdzenia w czasie działania, więc jeśli się mylisz, program wywali się później z TypeError.

Czym różni się ! od ? w TypeScript?

x! zapewnia, że wartość istnieje, i daje ci typ bez null, niczego nie sprawdzając. x?.y sprawdza w czasie działania: jeśli x to null lub undefined, zatrzymuje się i zwraca undefined. W deklaracji name?: string czyni właściwość opcjonalną, a name!: string mówi, że wymagana właściwość zostanie przypisana w miejscu, którego kompilator nie widzi.

Co oznacza let x!: string?

To definite assignment assertion. Mówi kompilatorowi, że zmienna zostanie przypisana przed odczytem, choć kompilator nie potrafi tego udowodnić (na przykład przypisanie dzieje się w innej funkcji). Bez niej odczyt zmiennej to błąd TS2454, Variable 'x' is used before being assigned.

Jak naprawić "has no initializer and is not definitely assigned in the constructor"?

To błąd TS2564 z strictPropertyInitialization. Nadaj właściwości wartość początkową, przypisz ją w konstruktorze, uczyń ją opcjonalną (prop?: T) albo, jeśli framework lub metoda inicjalizująca naprawdę ustawia ją przed użyciem, napisz prop!: T. ! to ostatnia opcja, bo nic nie pilnuje tej obietnicy.

Czy operator non-null assertion to zła praktyka?

Nie jest błędem, ale każdy ! to niesprawdzone twierdzenie. Konfiguracje lintera, takie jak @typescript-eslint/no-non-null-assertion, go zgłaszają. Lepiej użyć sprawdzenia, które zawęża typ (if (x), x ?? fallback, x?.y), albo funkcji pomocniczej rzucającej czytelny błąd. Zostaw ! na miejsca, gdzie wartość gwarantuje logika, której kompilator nie potrafi prześledzić.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ