Главная / Статьи / Операторы TypeScript на практике: математические, сравнительные, логические и тернарные

Операторы TypeScript на практике: математические, сравнительные, логические и тернарные

Практический обзор операторов арифметических вычислений, присваивания, сравнения, логических операций, увеличения и тернарных операторов в TypeScript, включая проверку равенства, которую компилятор отклоняет.

2635 слов

Как только вы поймете, как TypeScript присваивает и проверяет типы, следующим шагом будет работа с значениями: их сложение, сравнение, объединение условий и выбор между альтернативами. Операторы — это символы, которые выполняют эту работу, и большинство из них ведут себя точно так же, как и в JavaScript. Однако некоторые из них скрывают детали, которые часто приводят к ошибкам, такие как различия между слабым и строгим сравнением равенства, а также разница между x++ и ++x.

В этом руководстве рассматриваются арифметические, присваиваемые, относительные, логические, операторы увеличения и уменьшения, а также тернарные операторы с простыми примерами, которые можно проанализировать без их выполнения. По ходу вы увидите, где компилятор TypeScript добавляет дополнительные меры безопасности, отсутствующие в обычном JavaScript.

Сначала краткий обзор: несколько объявлений в одном операторе

Прежде чем рассматривать операторы, стоит упомянуть один синтаксический удобство, от которого зависят последующие примеры. Обычным способом объявления двух чисел является использование по одному оператору на переменную:

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

Один оператор let также может использоваться для объявления нескольких переменных сразу, если их разделить запятыми:

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

Обе формы компилируются в одинаковый результат; вторая просто более краткая. Каждая переменная в списке имеет свою собственную аннотацию, поэтому они могут иметь разные типы:

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

Используйте эту форму осторожно. Она хорошо читается для пары тесно связанных значений, таких как a и b, используемых в этом руководстве, но строка, сочетающая счётчик, имя и флаг, сложнее для восприятия, чем три отдельных объявления.

Арифметические операторы

Это знаки, знакомые нам по школьной математике. Примеры ниже начинаются с двух чисел:

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

Каждая строка применяет один оператор и показывает результат в комментарии:

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)

Что делает каждый оператор:

  • + складывает два операнда.
  • - вычитает правый операнд из левого.
  • * умножает их.
  • / делит их без округления. 10 / 20 действительно равно 0.5; отдельное целочисленное деление, как в некоторых других языках, отсутствует, поскольку каждое число в JavaScript является числом с плавающей точкой.
  • % возвращает остаток. 10 % 20 равно 10: двадцать входит в десять нулевой раз, поэтому весь десяток остается остатком.
  • ** поднимает левый операнд в степень правого. 10 ** 20 — это десять, умноженное на само себя двадцать раз, что объясняет крайне длинный результат.

Одно предупреждение относительно использования ** с большими значениями: числа, превышающие Number.MAX_SAFE_INTEGER (примерно девять квадриллионов), теряют точность целых чисел. Выражение 10 ** 20 выводится точно, но арифметические операции с такими значениями не являются надежными; используйте тип bigint, когда вам нужны точные большие целые числа.

Присваивание и сложное присваивание

Обычный оператор = сохраняет значение в переменной. JavaScript и TypeScript также предоставляют операторы сложного присваивания, которые выполняют арифметическую операцию и сохраняют результат в одном выражении. Начнем с двух значений:

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

Затем обновим a дважды: сначала традиционным способом, а затем с использованием сокращения:

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

Пошагово: a начинает с значения 100. Выражение a = a + b считывает текущее значение a, прибавляет к нему b (50) и записывает результат 150 обратно в a. Здесь нет ничего нового, кроме оператора +, рассмотренного в предыдущем разделе.

В последней строке используется a += b, что означает точно то же самое, что и a = a + b, только более компактно. Поскольку в этот момент a равно 150, после выполнения операции оно становится 200. Ассигнование также является выражением, значение которого — только что сохранённое число; поэтому передача a += b напрямую в console.log выводит 200. Это работает, но скрытие изменения состояния внутри аргумента функции затрудняет понимание кода, поэтому в реальном коде лучше выполнять обновление на отдельной строке.

Такая же схема применяется ко всем арифметическим операторам: -=, *=, /=, %= и **= — они вычитают, умножают, делят, находят остаток или вычисляют степень, а затем присваивают результат.

Операторы отношений

Операторы сравнения анализируют два значения и всегда возвращают логическое значение true или false. Вот операнды:

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

А вот сравнения:

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

Операторы сравнения понятны сами по себе: > (больше), < (меньше), >= (больше или равно) и <= (меньше или равно). Операторы равенства ==, === и != проверяют, совпадают ли два значения, но == и === работают по разным правилам, и эта разница заслуживает отдельного рассмотрения. Строгим аналогом != является !==, и именно его следует использовать в большинстве случаев.

Расслабленное равенство против строгого равенства

Эти два оператора равенства отличаются тем, как они обрабатывают типы:

  • == представляет собой расслабленное равенство. Если у операндов разные типы, он незаметно преобразует один из них перед сравнением значений.
  • === означает строгое равенство. Он требует одинакового типа и одинакового значения, не выполняет преобразований и возвращает false, когда типы различаются.
  • Чтобы показать это, возьмем число и строку, похожие друг на друга. Оба намеренно помечены как any; причина этого станет ясна вскоре.

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

    Сравнение их с помощью каждого оператора дает разные результаты:

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

    num1 содержит число 10, а num2 — строку "10". Расслабленное сравнение преобразует строку в число, находит 10 с обеих сторон и возвращает true. Строгое сравнение полностью игнорирует преобразование; число никогда не может быть равно строке, поэтому результат — false. Поскольку правила преобразования при расслабленном сравнении дают неожиданные результаты (например, 0 == "" равно true), большинство руководств по стилю и инструментов проверки кода рекомендуют использовать === и !== во всех случаях.

    Почему в примере нужен any

    Аннотации any не являются случайными. Если вы объявите переменные с их реальными типами, let num1: number = 10; и let num2: string = "10";, а затем напишете num1 === num2, код не скомпилируется. Компилятор сообщит об ошибке примерно в таком виде:

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

    TypeScript понимает, что значение типа number и значение типа string никогда не могут совпадать, поэтому считает такое сравнение ошибкой. Эта проверка применяется как к оператору ==, так и к оператору ===. Если обе переменные объявлены как any, анализ таких несовпадений отключается, и это единственная причина, по которой демонстрация компилируется. Это небольшой, но полезный пример того, как система типов предотвращает бессмысленные сравнения до запуска программы, чего обычный JavaScript никогда не сообщает. Это также показывает, почему использование any имеет негативные последствия: оно бесшумно отключает именно такую защиту. Для альтернатив см. шесть типобезопасных замен any.

    Логические операторы

    Логические операторы объединяют условия. Их существует три: && (И), || (ИЛИ) и ! (НЕ). Начните с двух логических значений:

    let b1 = true;
    let b2 = false;
    

    Затем объедините их и примените оператор НЕ:

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

    Полное поведение операторов И и ИЛИ для каждой пары входных значений:

    • true и true: && возвращает true, || также возвращает true.
    • true и false: && возвращает false, || возвращает true.
    • false и true: && возвращает false, || возвращает true.
    • false и false: && возвращает false, || также возвращает false.

    А для оператора NOT:

    • !true равно false.
    • !false равно true.

    По-простому:

    • && (И) — строгий оператор. Он возвращает true, только если оба значения равны true; наличие хотя бы одного false приводит к результату false.
    • || (ИЛИ) — более гибкий оператор. Достаточно наличия хотя бы одного true; результат будет false только тогда, когда оба значения равны false.
    • ! (NOT) принимает один операнд и инвертирует его значение: true превращается в false, и наоборот.

    Важный момент: ! всегда возвращает логическое значение, тогда как && и || на самом деле возвращают один из своих операндов. При использовании логических значений результат всегда остается логическим, как и ранее. При других значениях выражение "" || "default" дает результат "default", а выражение user && user.name — либо user, либо user.name. Оба оператора также работают по принципу сокращенной обработки: если результат уже определён на левой стороне, правая сторона никогда не обрабатывается. Именно поэтому их часто используют для задания значений по умолчанию и проверки условий, причём TypeScript соответственно задаёт тип результатов.

    Сочетание сравнений с логическими операциями

    Относительные и логические операторы обычно используются вместе: сравнения генерируют логические значения, а логические операторы соединяют их.

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

    В первой строке 20 > 10 равно true, а 20 < 10 — false. Оператор И требует, чтобы оба выражения были true, поэтому результат — false.

    Во второй строке оба сравнения одинаковы, но оператор ИЛИ требует лишь наличия хотя бы одного true, поэтому результат — true. Поскольку операторы сравнения имеют более высокий приоритет, чем && и ||, здесь не нужны скобки, хотя их использование может улучшить читаемость более сложных условий.

    Увеличение и уменьшение

    ++ добавляет 1 к числу, а -- вычитает 1. Каждый из этих операторов существует в двух формах — префиксной и постфиксной, и различие между ними часто вызывает путаницу. Начнем с одной переменной:

    let x: number = 10;
    

    Когда оператор используется как отдельное выражение, обе формы дают одинаковый результат:

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

    Если записать x++ и ++x на отдельных строках, оба варианта просто увеличивают значение x на один. Разница в их поведении проявляется только тогда, когда значение выражения используется в том же операторе, например при присваивании его другой переменной. Именно тогда становится важным порядок применения оператора увеличения.

    Увеличение с помощью постфикса: сначала используется значение, затем происходит увеличение

    «Постфикс» означает «после»: сначала выражение возвращает текущее значение переменной, а увеличение происходит позже.

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

    Печать обеих переменных показывает результат действия:

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

    Порядок событий следующий:

    1. Считывается текущее значение x, равное 10.
    2. Это значение становится результатом выражения и сохраняется в res.
    3. Затем значение x увеличивается до 11.

    Таким образом, res сохраняет старое значение 10, в то время как x принимает значение 11. Увеличение действительно происходит, но только после копирования значения.

    Увеличение с префиксом: сначала прибавляем один, затем используем значение

    «Pre» означает «до»: сначала переменная увеличивается, а выражение возвращает обновленное значение.

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

    Результат подтверждает это:

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

    Здесь порядок обратный:

    1. x меняется с 10 на 11.
    2. Новое значение 11 сохраняется в res.

    Обе переменные в итоге имеют значение 11. Кратко: x++ возвращает значение до увеличения, а ++x — значение после него.

    Уменьшение следует тем же правилам

    -- является аналогом ++, но выполняет вычитание вместо сложения. Вот форма с префиксом:

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

    А результат её работы:

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

    При префиксном уменьшении значение x сначала становится 9, и это новое значение сохраняется в res; поэтому оба значения выводятся как 9. Форма с постфиксом x-- действует наоборот: сначала в res попадает первоначальное значение 10, а уже затем x становится 9. Многие команды вообще избегают использования ++ и -- в более сложных выражениях, именно потому что различие между префиксной и постфиксной формами легко неправильно интерпретировать; выражение x += 1 в отдельной строке никогда не вызывает двусмысленности.

    Троичный условный оператор

    Оператор тернарного выбора позволяет выразить простое условие if/else в виде одного выражения. Он использует символы ? и : и является единственным оператором в JavaScript, принимающим три операнда, откуда и происходит его название. Начнем с двух чисел:

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

    Затем выберем более большое из них:

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

    Выражение состоит из трех частей:

    1. (a > b) перед символом ? — это условие.
    2. a, следующий сразу за ?, — это результат, когда условие истинно.
    3. b, следующий за символом :, — это результат, когда условие ложно.

    Если прочитать вслух, там написано: если a больше b, использовать a, в противном случае — b. Поскольку 100 не больше 200, условие оказывается false, и res принимает значение 200.

    Замена блока if/else

    Основное преимущество заключается в краткости. Та же логика, записанная в виде отдельного предложения, занимает несколько строк и требует объявления res перед его присваиванием:

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

    Тернарная версия выполняет ту же задачу в одной строке. Эти два фрагмента являются альтернативами и не предназначены для вставки в один и тот же контекст, поскольку двойное объявление res с помощью let приведёт к ошибке:

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

    Поскольку тернарная конструкция является выражением, она позволяет одновременно объявить переменную и присвоить ей начальное значение, а TypeScript определяет или проверяет тип результата на основе обоих вариантов.

    Сопоставление значения с меткой

    Троичные операторы также эффективно работают, когда ветви имеют другой тип, чем условие. В этом примере возраст преобразуется в описательную строку:

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

    30 > 18 равно true, поэтому res будет равен "adult". При возрасте 15 условие будет равно false, и res станет "minor".

    Используйте троичные операторы для простых, однократных решений вроде этих. Как только вы начнете вкладывать один троичный оператор в другой для обработки нескольких случаев, читаемость быстро снижается, и цепочка if...else if...else (или объект поиска) обычно бывает понятнее.

    Основные выводы

    • Операторы TypeScript — это операторы JavaScript; компилированный результат ведет себя одинаково во время выполнения.
  • / никогда не обрезает значение, % возвращает остаток от деления, а слишком большие результаты операции ** теряют точность, если не использовать тип bigint.
  • Сложные присваивания, такие как +=, представляют собой сокращение для операции «вычислить и сохранить обратно».
  • Лучше использовать === и !==; операторы слабого равенства преобразуют типы в неожиданных случаях.
  • Компилятор отклоняет сравнения между типами, которые никогда не могут совпадать, а оператор any молча устраняет это ограничение.
  • Операторы && и || работают по принципу краткого замыкания и возвращают один из операндов, поэтому они также могут использоваться в качестве защитных конструкций и значений по умолчанию.
  • Постфиксная форма x++ возвращает старое значение, а префиксная форма ++x — новое; разница проявляется только тогда, когда используется результат операции.
  • Используйте тернарный оператор для одного четкого выбора, а когда условия начнут множиться, вернитесь к if...else.