Home / Articles / TypeScript Operators in Practice: Math, Comparisons, Logic and Ternaries

This article is published in English.

TypeScript Operators in Practice: Math, Comparisons, Logic and Ternaries

A practical tour of TypeScript's arithmetic, assignment, comparison, logical, increment and ternary operators, including the equality check the compiler refuses.

2635 words

Once you know how TypeScript assigns and enforces types, the next step is doing something with the values: adding them, comparing them, combining conditions and choosing between alternatives. Operators are the symbols that perform that work, and most of them behave exactly as they do in JavaScript. A few, however, hide details that regularly cause bugs, such as loose versus strict equality and the difference between x++ and ++x.

This guide covers arithmetic, assignment, relational, logical, increment and decrement, and ternary operators with small examples you can reason about without running them. Along the way you will see where the TypeScript compiler adds a safety net that plain JavaScript does not have.

A shortcut first: several declarations in one statement

Before looking at operators, one syntax convenience that the later examples rely on. The usual way to declare two numbers is one statement per variable:

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

A single let can also introduce several variables at once if you separate them with commas:

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

The two forms compile to the same thing; the second is simply shorter. Each variable in the list carries its own annotation, so they are free to have different types:

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

Use this form sparingly. It reads well for a pair of closely related values, like the a and b used throughout this guide, but a line that mixes a counter, a name and a flag is harder to scan than three separate declarations.

Arithmetic operators

These are the familiar symbols from school maths. The examples below start with two numbers:

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

Each line then applies one operator and shows the result in a comment:

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)

What each one does:

  • + adds the two operands.
  • - subtracts the right operand from the left one.
  • * multiplies them.
  • / divides them without rounding. 10 / 20 really is 0.5; there is no separate integer division as in some other languages, because every JavaScript number is a floating-point value.
  • % returns the remainder. 10 % 20 is 10: twenty goes into ten zero times, so all of the ten is left over.
  • ** raises the left operand to the power of the right. 10 ** 20 is ten multiplied by itself twenty times, which explains the very long result.

One caution with ** and large results: numbers beyond Number.MAX_SAFE_INTEGER (roughly nine quadrillion) lose integer precision. 10 ** 20 happens to print exactly, but arithmetic on values of that size is not reliable; use bigint when you need exact large integers.

Assignment and compound assignment

The plain = stores a value in a variable. JavaScript and TypeScript also provide compound assignment operators that perform an arithmetic step and store the result in a single expression. Start with two values:

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

Then update a twice, once the long way and once with the shorthand:

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

Step by step: a begins at 100. The statement a = a + b reads the current a, adds b (50) and writes 150 back into a. Nothing new here beyond the + operator from the previous section.

The final line uses a += b, which means exactly the same as a = a + b, only written more compactly. Since a is 150 at that point, it becomes 200. An assignment is also an expression whose value is the newly stored number, which is why passing a += b straight to console.log prints 200. That works, but hiding a state change inside a function argument makes code harder to follow, so in real code prefer doing the update on its own line.

The same pattern exists for every arithmetic operator: -=, *=, /=, %= and **= subtract, multiply, divide, take the remainder or exponentiate, then assign.

Relational operators

Comparison operators look at two values and always produce a boolean, true or false. Here are the operands:

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

And the comparisons:

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

The ordering operators are self-explanatory: > (greater than), < (less than), >= (greater than or equal to) and <= (less than or equal to). The equality operators ==, === and != test whether two values match, but == and === follow different rules, and that difference is worth its own section. The strict counterpart of != is !==, and it is the one you should normally reach for.

Loose equality versus strict equality

The two equality operators differ in how they treat types:

  • == is loose equality. If the operands have different types, it silently converts one of them before comparing the values.
  • === is strict equality. It requires the same type and the same value, performs no conversion, and returns false whenever the types differ.

To demonstrate, take a number and a string that look alike. Both are deliberately annotated as any; the reason follows shortly.

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

Comparing them with each operator gives different answers:

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

num1 holds the number 10 and num2 holds the string "10". Loose equality converts the string to a number, finds 10 on both sides and returns true. Strict equality skips conversion entirely; a number is never equal to a string, so the result is false. Because loose equality's conversion rules produce surprising results (for example, 0 == "" is true), most style guides and linters recommend using === and !== everywhere.

Why the example needs any

The any annotations are not incidental. If you declare the variables with their real types, let num1: number = 10; and let num2: string = "10";, and then write num1 === num2, the code does not compile. The compiler reports an error along these lines:

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

TypeScript knows that a value typed number and a value typed string can never be the same, so it treats the comparison as a mistake. The check applies to == as well as ===. Typing both variables as any disables that overlap analysis, which is the only reason the demonstration compiles. It is a small but useful example of the type system catching a pointless comparison before the program runs, something plain JavaScript never warns about. It also shows why any is costly: it quietly switches off exactly this kind of protection. For alternatives, see six type-safe replacements for any.

Logical operators

Logical operators combine conditions. There are three: && (AND), || (OR) and ! (NOT). Start with two booleans:

let b1 = true;
let b2 = false;

Then combine and negate them:

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

The full behaviour of AND and OR for every pair of inputs:

  • true and true: && gives true, || gives true.
  • true and false: && gives false, || gives true.
  • false and true: && gives false, || gives true.
  • false and false: && gives false, || gives false.

And for NOT:

  • !true is false.
  • !false is true.

In words:

  • && (AND) is strict. It returns true only if both sides are true; one false makes the whole expression false.
  • || (OR) is lenient. A single true side is enough, and it returns false only when both sides are false.
  • ! (NOT) takes a single operand and flips it: true becomes false and vice versa.

A detail worth knowing: ! always returns a boolean, but && and || actually return one of their operands. With boolean inputs that is always a boolean, as above. With other values, "" || "default" evaluates to "default" and user && user.name evaluates to either user or user.name. Both operators also short-circuit: if the left side already decides the result, the right side is never evaluated. That behaviour is why they are often used for defaults and guards, and TypeScript types the result accordingly.

Combining comparisons with logic

Relational and logical operators usually appear together: comparisons produce the booleans, and logical operators combine them.

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

In the first line, 20 > 10 is true and 20 < 10 is false. AND needs both to be true, so the result is false.

In the second line the two comparisons are the same, but OR needs only one true side, so the result is true. Because comparison operators bind more tightly than && and ||, no parentheses are needed here, though adding them can make longer conditions easier to read.

Increment and decrement

++ adds 1 to a number and -- subtracts 1. Each comes in two forms, prefix and postfix, and the distinction is a classic source of confusion. Begin with a single variable:

let x: number = 10;

When the operator is used as a statement on its own, both forms have the same effect:

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

Written on its own line, x++ and ++x both simply add one to x. The two forms only behave differently when the expression's value is used in the same statement, for example when you assign it to another variable. That is where prefix and postfix start to matter.

Postfix increment: use the value, then add one

"Post" means after: the expression evaluates to the variable's current value, and the increment happens afterwards.

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

Printing both variables shows the effect:

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

The order of events is:

  1. The current value of x, 10, is read.
  2. That value becomes the result of the expression and is stored in res.
  3. Then x is increased to 11.

So res keeps the old value, 10, while x moves on to 11. The increment did happen; it just happened after the value was copied.

Prefix increment: add one, then use the value

"Pre" means before: the variable is incremented first, and the expression evaluates to the updated value.

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

The output confirms it:

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

Here the order is reversed:

  1. x goes from 10 to 11.
  2. The new value, 11, is stored in res.

Both variables end up as 11. The short version: x++ produces the value from before the increment, ++x produces the value from after it.

Decrement follows the same rules

-- mirrors ++, subtracting instead of adding. Here is the prefix form:

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

And its output:

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

With prefix decrement, x drops to 9 first and that new value goes into res, so both print 9. The postfix form x-- would do the opposite: res would receive the original 10, and only then would x fall to 9. Many teams avoid using ++ and -- inside larger expressions altogether, precisely because the prefix/postfix distinction is easy to misread; x += 1 on its own line is never ambiguous.

The ternary conditional operator

The ternary operator expresses a simple if/else choice as a single expression. It uses ? and :, and it is the only JavaScript operator that takes three operands, which is where the name comes from. Start with two numbers:

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

Then pick the larger one:

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

The expression has three parts:

  1. (a > b) before the ? is the condition.
  2. a, directly after the ?, is the result when the condition is true.
  3. b, after the :, is the result when the condition is false.

Read aloud, it says: if a is greater than b, use a, otherwise use b. Since 100 is not greater than 200, the condition is false and res becomes 200.

Replacing an if/else block

The main attraction is brevity. The same logic written as a statement takes several lines and requires declaring res before assigning it:

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

The ternary version does the same in one line. The two snippets are alternatives, not code to paste into the same scope, since declaring res twice with let would be an error:

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

Because the ternary is an expression, it also lets you declare the variable and initialise it in one step, and TypeScript infers or checks the type of the result from both branches.

Mapping a value to a label

Ternaries work equally well when the branches have a different type from the condition. This example turns an age into a descriptive string:

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

30 > 18 is true, so res is "adult". With an age of 15, the condition would be false and res would be "minor".

Keep ternaries for single, simple decisions like these. Once you start nesting one ternary inside another to cover several cases, readability drops quickly, and an if...else if...else chain (or a lookup object) is usually clearer.

Key takeaways

  • TypeScript's operators are JavaScript's operators; the compiled output behaves identically at runtime.
  • / never truncates, % returns the remainder, and very large results from ** lose precision unless you use bigint.
  • Compound assignments such as += are shorthand for "compute and store back".
  • Prefer === and !==; loose equality converts types in surprising ways.
  • The compiler rejects comparisons between types that can never overlap, and any silently removes that protection.
  • && and || short-circuit and return an operand, which is why they double as guards and defaults.
  • Postfix x++ yields the old value, prefix ++x the new one; the difference only shows when the result is used.
  • Use the ternary for one clear choice, and switch back to if...else once conditions start stacking up.