Strona główna / Artykuły / Operatorzy TypeScript w praktyce: matematyka, porównania, logika i operatory ternarne

Operatorzy TypeScript w praktyce: matematyka, porównania, logika i operatory ternarne

Praktyczny przewodnik po operatorach arytmetycznych, przydzielania, porównywania, logicznych, inkrementacji oraz ternarnych w TypeScript, w tym o sprawdzaniu równości, którego kompilator odmawia.

2635 słów

Gdy już poznasz, w jaki sposób TypeScript przypisuje i egzekwuje typy, następnym krokiem jest wykonywanie jakichś działań z wartościami: ich dodawanie, porównywanie, łączenie warunków oraz wybieranie między alternatywami. Operatory to symbole, które wykonują te operacje, a większość z nich zachowuje się dokładnie tak samo jak w JavaScript. Niektóre jednak ukrywają szczegóły, które regularnie powodują błędy, takie jak różnica między równością luźną a ścisłą oraz różnica pomiędzy x++ a ++x.

To przewodnik omawia operatory arytmetyczne, przypisania, relacyjne, logiczne, inkrementacji i dekrementacji oraz operator ternarny, przytaczając krótkie przykłady, nad którymi można się zastanowić bez ich uruchamiania. Po drodze zobaczysz, gdzie kompilator TypeScript dodaje mechanizmy bezpieczeństwa, których brakuje w zwykłym JavaScript.

Najpierw skrót: kilka deklaracji w jednym zdaniu

Zanim przejdziemy do operatorów, warto wspomnieć o jednej wygodzie składniowej, od której zależą późniejsze przykłady. Zwykły sposób deklarowania dwóch liczb polega na użyciu jednego instrukcji na każdą zmienną:

let a: number = 10;
let b: number = 20;

Jedno słowo kluczowe let może również wprowadzić kilka zmiennych jednocześnie, jeśli oddzielimy je przecinkami:

let a: number = 10, b: number = 20;

Oba te sposoby skompilują się w to samo; drugi jest po prostu krótszy. Każda zmienna na liście ma własną adnotację, więc mogą one mieć różne typy:

let a: number = 10, name: string = "John", isActive: boolean = true;

Należy używać tego sposobu z umiarem. Jest on dobrze czytelny w przypadku par wartości ściśle powiązanych, takich jak a i b używane w tym przewodniku, ale linia łącząca licznik, nazwę i flagę jest trudniejsza do przeczytania niż trzy oddzielne deklaracje.

Operatory arytmetyczne

Są to znane symbole z matematyki szkolnej. Poniższe przykłady rozpoczynają się od dwóch liczb:

// Arithmetic operators
let a: number = 10, b: number = 20;

Każdy wiersz stosuje wtedy jeden operator i pokazuje wynik w komentarzu:

console.log(a + b);   // 30   → addition
console.log(a - b);   // -10  → subtraction
console.log(a * b);   // 200  → multiplication
console.log(a / b);   // 0.5  → division
console.log(a % b);   // 10   → modulus (remainder after division)
console.log(a ** b);  // 100000000000000000000 → exponentiation (10 to the power 20)

Co robi każdy z nich:

  • + dodaje dwa operandy.
  • - odejmuje prawy operand od lewego.
  • * mnoży je ze sobą.
  • / dzieli je bez zaokrąglenia. 10 / 20 to rzeczywiście 0.5; nie ma oddzielnej dzielenia całkowitego jak w niektórych innych językach, ponieważ każda liczba w JavaScript jest wartością zmiennoprzecinkową.
  • % zwraca resztę. 10 % 20 to 10: dwadzieścia razy nie da się wziąć dziesięciu, więc cała dziesiątka pozostaje.
  • ** podnosi lewy operand do potęgi prawego. 10 ** 20 to dziesięć pomnożone przez same siebie dwadzieścia razy, co tłumaczy bardzo długi wynik.

Jedna uwaga dotycząca ** i dużych wyników: liczby przekraczające Number.MAX_SAFE_INTEGER (około dziewięciu kwadrilionów) tracą precyzję całkowitą. 10 ** 20 jest wyświetlany dokładnie, ale operacje arytmetyczne na tak dużych wartościach nie są wiarygodne; użyj bigint, gdy potrzebujesz dokładnych dużych liczb całkowitych.

Przypisywanie i złożone przypisywanie

Zwykłe = przechowuje wartość w zmiennej. JavaScript i TypeScript dostarczają również operatorów złożonego przypisywania, które wykonywają operację arytmetyczną i przechowują wynik w jednym wyrażeniu. Zacznijmy od dwóch wartości:

let a: number = 100, b: number = 50;

Następnie zaktualizuj a dwa razy – raz w tradycyjny sposób, a raz za pomocą skrótu:

console.log(a);      // 100
a = a + b;
console.log(a);      // 150
console.log(a += b); // 200

Krok po kroku: a zaczyna od wartości 100. Wyrażenie a = a + b odczytuje aktualną wartość a, dodaje b (50) i zapisuje wartość 150 z powrotem do a. Nie ma tu nic nowego poza operatorem + omówionym w poprzedniej sekcji.

Ostatni wiersz używa a += b, co oznacza dokładnie to samo co a = a + b, tylko jest napisane w bardziej zwięzły sposób. Ponieważ a ma w tym momencie wartość 150, staje się ona 200. Przypisanie to również jest wyrażeniem, którego wartością jest nowo zapisana liczba, dlatego przekazanie a += b bezpośrednio do console.log wyświetla wartość 200. To działa, ale ukrywanie zmiany stanu wewnątrz argumentu funkcji utrudnia zrozumienie kodu, dlatego w prawdziwym kodzie lepiej wykonywać takie aktualizacje na osobnym wierszu.

Ten sam schemat obowiązuje dla każdego operatora arytmetycznego: -=, *=, /=, %= oraz **= – one odejmują, mnożą, dzielą, obliczają resztę lub wykonywają operację potęgowania, a następnie przypisują wartość.

Operatorzy relacyjne

Operatory porównawcze analizują dwa wartości i zawsze zwracają wartość logiczną, true lub false. Oto operandy:

let a: number = 10;
let b: number = 20;

A porównania:

console.log(a > b);   // false
console.log(a < b);   // true
console.log(a === b); // false
console.log(a >= b);  // false
console.log(a <= b);  // true
console.log(a == b);  // false
console.log(a != b);  // true

Operatorzy porządkowe są oczywiste: > (większe od), < (mniejsze od), >= (większe lub równe) oraz <= (mniejsze lub równe). Operatorzy równości ==, === i != sprawdzają, czy dwa wartości są identyczne, ale == i === działają według innych zasad, a ta różnica wymaga osobnego omówienia. Ścisłym odpowiednikiem != jest !==, i to właśnie go należy zazwyczaj używać.

Równość luźna versus równość ścisła

Dwa operatorzy równości różnią się sposobem traktowania typów:

  • == to równość luźna. Jeśli operandy mają różne typy, jeden z nich jest cicho konwertowany przed porównaniem wartości.
  • === oznacza ściśłą równość. Wymaga tego samego typu i tej samej wartości, nie przeprowadza żadnych konwersji i zwraca false, gdy typy się różnią.
  • Aby to pokazać, weźmy liczbę i ciąg znaków, które wyglądają podobnie. Oba są celowo oznaczone jako any; powód zostanie wkrótce wyjaśniony.

    let num1: any = 10;
    let num2: any = "10";
    

    Porównanie ich za pomocą poszczególnych operatorów daje różne wyniki:

    console.log(num1 == num2);  // true  → same value, type is ignored
    console.log(num1 === num2); // false → same value, but number ≠ string
    

    num1 przechowuje liczbę 10, a num2 przechowuje ciąg znaków "10". Równość luźna przekształca ciąg znaków na liczbę, znajduje 10 po obu stronach i zwraca true. Równość ścisła całkowicie pomija tę konwersję; liczba nigdy nie może być równa ciągowi znaków, więc wynik jest false. Ponieważ zasady konwersji w przypadku równości luźnej dają zaskakujące wyniki (na przykład 0 == "" daje true), większość przewodników stylowych i narzędzi sprawdzających kod zaleca używanie === i !== we wszystkich przypadkach.

    Dlaczego przykład wymaga użycia any

    Adnotacje any nie są przypadkowe. Jeśli zadeklarujesz zmienne z ich rzeczywistymi typami, let num1: number = 10; i let num2: string = „10“;, a następnie napiszesz num1 === num2, kod nie zostanie skompilowany. Kompilator zgłosi błąd w przybliżeniu w tych słowach:

    This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.
    

    TypeScript wie, że wartość typu number i wartość typu string nigdy nie mogą być takie same, dlatego traktuje to porównanie jako błąd. Ta kontrola dotyczy zarówno operatora ==, jak i ===. Ustawienie obu zmiennych na typ any wyłącza tę analizę, co jest jedynym powodem, dla którego demonstracja kompiluje się. Jest to mały, ale przydatny przykład systemu typów wykrywającego bezsensowne porównania przed uruchomieniem programu – czego zwykły JavaScript nigdy nie ostrzega. Pokazuje również, dlaczego użycie typu any jest kosztowne: w tajemnicy wyłącza właśnie ten rodzaj ochrony. Aby zapoznać się z alternatywami, zobacz sześć bezpiecznych zamienników typu any.

    Operatory logiczne

    Operatorzy logiczne łączą warunki. Istnieją trzy: && (AND), || (OR) oraz ! (NOT). Zaczynamy od dwóch wartości logicznych:

    let b1 = true;
    let b2 = false;
    

    Następnie łączymy je i negujemy:

    console.log(b1 && b2); // false
    console.log(b1 || b2); // true
    console.log(!b2);      // true
    console.log(!b1);      // false
    

    Pełne zachowanie operacji AND i OR dla każdej pary wartości wejściowych:

    • true i true: && zwraca true, || również zwraca true.
    • true i false: && zwraca false, || zwraca true.
    • false i true: && zwraca false, || zwraca true.
    • false i false: && zwraca false, || również zwraca false.

    A dla NOT:

    • !true to false.
    • !false to true.

    Mówiąc prościej:

    • && (I) jest ścisły – zwraca true tylko wtedy, gdy obie strony są true; jedna wartość false sprawia, że całe wyrażenie jest false.
    • || (LUB) jest łagodny – wystarczy jedna wartość true, a zwraca false tylko wtedy, gdy obie strony są false.
    • ! (NOT) przyjmuje jeden operand i odwraca jego wartość: true staje się false i odwrotnie.

    Detal, który warto znać: ! zawsze zwraca wartość logiczną, natomiast && i || faktycznie zwracają jeden z swoich operandów. W przypadku operandów logicznych jest to zawsze wartość logiczna, jak opisano powyżej. Przy innych wartościach "" || "default" zwraca "default", a user && user.name zwraca albo user, albo user.name. Obaj operatorzy działają również w trybie skróconym: jeśli strona lewa już określi wynik, strona prawa nie jest w ogóle ewaluowana. To właśnie dzięki temu zachowaniu są one często używane do ustawiania wartości domyślnych i warunków, a TypeScript odpowiednio typuje ich wynik.

    Łączenie porównań z logiką

    Operatorzy relacyjne i logiczne zazwyczaj występują razem: porównania tworzą wartości logiczne, a operatory logiczne łączą je ze sobą.

    console.log(20 > 10 && 20 < 10); // false
    console.log(20 > 10 || 20 < 10); // true
    

    W pierwszym wierszu 20 > 10 daje wartość true, a 20 < 10 daje false. Operator AND wymaga, aby obie wartości były true, więc wynik jest false.

    W drugim wierszu obie porównania są takie same, ale operator OR wymaga tylko jednej wartości true, więc wynik jest true. Ponieważ operatorzy porównania łączą się mocniej niż && i ||, nie są tu potrzebne nawiasy, chociaż ich dodanie może ułatwić czytanie bardziej złożonych warunków.

    Zwiększanie i zmniejszanie

    ++ dodaje 1 do liczby, a -- odejmuje 1. Każdy z tych operatorów występuje w dwóch formach: przedrostkowej i tylnoprzedrostkowej, a różnica między nimi jest częstym źródłem zamieszania. Zacznijmy od pojedynczej zmiennej:

    let x: number = 10;
    

    Gdy operator jest używany samodzielnie jako instrukcja, obie formy dają ten sam efekt:

    x++; // same as x = x + 1
    console.log(x); // 11
    ++x; // same as x = x + 1
    console.log(x); // 12
    

    Napisane na osobnej linii, x++ i ++x oba po prostu dodają jedynkę do x. Te dwa formy zachowują się inaczej tylko wtedy, gdy wartość wyrażenia jest używana w tej samej instrukcji, na przykład gdy przypisuje się ją do innej zmiennej. Wtedy właśnie przydają się przedrostek i sufiks.

    Zwiększanie wartości sufiksem: najpierw użyj wartości, potem dodaj jedynkę

    "Sufiks" oznacza później: wyrażenie jest wyceniane na podstawie aktualnej wartości zmiennej, a zwiększenie następuje później.

    let x: number = 10;
    let res: number = x++;
    

    Wyświetlenie obu zmiennych pokazuje efekt:

    console.log(res); // 10
    console.log(x);   // 11
    

    Kolejność zdarzeń jest następująca:

    1. Odczytuje się aktualną wartość x, czyli 10.
    2. Ta wartość staje się wynikiem wyrażenia i jest przechowywana w res.
    3. Następnie x jest zwiększane do 11.

    Zatem res zachowuje stare wartość, 10, podczas gdy x przyjmuje wartość 11. Zwiększenie rzeczywiście nastąpiło; po prostu wydarzyło się po skopiowaniu wartości.

    Zwiększanie z prefiksem: dodaj jeden, a następnie użyj wartości

    "Pre" oznacza przed: zmienna jest najpierw zwiększana, a wyrażenie przyjmuje wartość po aktualizacji.

    let x: number = 10;
    let res: number = ++x;
    

    Wynik to potwierdza:

    console.log(res); // 11
    console.log(x);   // 11
    

    Tutaj kolejność jest odwrotna:

    1. x zmienia się z 10 na 11.
    2. Nowa wartość, 11, jest przechowywana w res.

    Oba zmienne ostatecznie mają wartość 11. Krótka wersja: x++ zwraca wartość sprzed zwiększenia, natomiast ++x – wartość po nim.

    Zmniejszanie podlega tym samym zasadom

    -- jest odpowiednikiem ++, ale odejmuje zamiast dodawać. Oto forma z prefiksem:

    let x: number = 10;
    let res: number = --x;
    

    A jej wynik:

    console.log(res); // 9
    console.log(x);   // 9
    

    Przy dekrementacji z prefiksem x najpierw spada do 9, a ta nowa wartość trafia do res, więc obie zmienne wyświetlają wartość 9. Forma z postfixem x-- działałaby odwrotnie: res otrzymałby początkową wartość 10, a dopiero potem x spadłby do 9. Wiele zespołów unika używania ++ i -- w większych wyrażeniach, właśnie dlatego, że rozróżnienie między formą z prefiksem a postfixem łatwo jest błędnie zinterpretować; x += 1 na osobnej linii nigdy nie jest dwuznaczne.

    Operator warunkowy trywialny

    Operator ternarny wyraża prostą opcję if/else jako jedną wyrażenie. Wykorzystuje ? i :, i jest jedynym operatorem w JavaScript, który przyjmuje trzy operandy – stąd pochodzi jego nazwa. Zacznijmy od dwóch liczb:

    let a: number = 100;
    let b: number = 200;
    

    Następnie wybierzmy tę większą:

    let res: number = (a > b) ? a : b;
    console.log(res); // 200
    

    Wyrażenie składa się z trzech części:

    1. (a > b) przed ? to warunek.
    2. a, bezpośrednio po ?, to wynik, gdy warunek jest true.
    3. b, po :, to wynik, gdy warunek jest false.

    Czytając na głos, widnieje tam napis: jeśli a jest większe od b, użyj a, w przeciwnym razie użyj b. Ponieważ 100 nie jest większe od 200, warunek wynosi false, a res przyjmuje wartość 200.

    Zastępowanie bloku if/else

    Główną zaletą jest zwięzłość. Ta sama logika zapisaana jako instrukcja zajmuje kilka wierszy i wymaga deklaracji res przed przypisaniem mu wartości:

    // Without ternary
    let res: number;
    if (a > b) {
      res = a;
    } else {
      res = b;
    }
    

    Wersja ternarna realizuje to w jednym wierszu. Obie wersje są alternatywami, a nie kodem do wklejenia w ten sam kontekst, ponieważ dwukrotne deklarowanie res za pomocą let spowodowałoby błąd:

    // With ternary — same result, one line
    let res: number = (a > b) ? a : b;
    

    Ponieważ wyrażenie ternarne jest również wyrażeniem, umożliwia ono jednoczesną deklarację zmiennej i jej inicjalizację, a TypeScript wywnioskowuje lub sprawdza typ wyniku na podstawie obu gałęzi.

    Przyporządkowanie wartości do etykiety

    Wyrażenia ternarne działają równie dobrze, gdy gałęzie mają inny typ niż warunek. Ten przykład przekształca wiek na opisowy ciąg znaków:

    let personage: number = 30;
    let res: string = (personage > 18) ? "adult" : "minor";
    console.log(res); // "adult"
    

    30 > 18 jest true, więc res to "dorosły". Przy wieku 15 warunek byłby false, a res to "nieletni".

    Używaj wyrażeń ternarnych do prostych, pojedynczych decyzji takich jak te. Gdy zaczniesz układać jedno wyrażenie ternarne wewnątrz drugiego, aby obsłużyć kilka przypadków, czytelność szybko spada – wtedy łańcuch if...else if...else (lub obiekt wyszukiwania) jest zazwyczaj jaśniejszy.

    Główne wnioski

    • Operatorzy TypeScript są tymi samymi co w JavaScript; skompilowany kod zachowuje się identycznie podczas wykonywania.
  • / nigdy nie skraca wartości, % zwraca resztę, a bardzo duże wyniki uzyskane za pomocą ** tracą precyzję, chyba że użyjesz typu bigint.
  • Atrybucje złożone, takie jak +=, są skrótem dla operacji „oblicz i zapisz z powrotem”.
  • Lepiej używać === i !==; luźna równość przekształca typy w zaskakujący sposób.
  • Kompilator odrzuca porównania między typami, które nigdy nie mogą się pokrywać, a any w tajemnicy usuwa tę ochronę.
  • && i || działają w trybie skróconym i zwracają jeden z operandów, dlatego mogą służyć zarówno jako mechanizmy ochronne, jak i wartości domyślne.
  • Postfiks x++ zwraca staryą wartość, a prefiks ++x nową; różnica jest widoczna tylko wtedy, gdy używa się wyniku.
  • Użyj struktury ternarnej do jednego wyraźnego wyboru, a gdy warunki zaczną się mnożyć, wróć do if...else.
  • Literatura pokrewna