TypeScript Logical Operators: &&, ||, !, ?? and ?.

How &&, || and ! short-circuit and return operands in TypeScript, when to use ?? instead of ||, optional chaining, logical assignment, and the compiler checks for always-truthy conditions.

angular-typescript

TypeScript logical operators combine and invert conditions, and the operators && and || return one of their operands instead of a plain true or false. TypeScript uses the JavaScript set of logical operators.

  • The classic operators are AND (&&), OR (||) and NOT (!).
  • The newer nullish coalescing operator (??) returns a default only for null or undefined.
  • The logical assignment operators &&=, ||= and ??= assign a value only when needed.

We use logical operators to combine permission checks and to fill in default values when a value is missing. For example, an edit button shows only when the user is an admin and the account is active.

The following example shows the basic operators, the difference between || and ??, optional chaining and the assignment forms. The code targets ES2024 and runs with TypeScript 7.0.2 on Node.js 22.

const isAdmin: boolean = true;
const isActive: boolean = false;

// 1. AND, OR, NOT
const canEdit = isAdmin && isActive;          // canEdit = false
const canView = isAdmin || isActive;          // canView = true
const isBlocked = !isActive;                  // isBlocked = true

// 2. || replaces any falsy value, ?? only null and undefined
const count: number = 0;
const shown = count || 10;                    // shown = 10
const kept = count ?? 10;                     // kept = 0

// 3. Optional chaining with a default
const user: { address?: { city: string } } = {};
const city = user.address?.city ?? "Unknown"; // city = "Unknown"

// 4. Logical assignment
let retries: number | undefined;
retries ??= 3;                                // retries = 3
let title = "";
title ||= "Untitled";                         // title = "Untitled"
let total = 5;
total &&= total * 2;                          // total = 10

Notice that count || 10 replaces the valid 0, whereas count ?? 10 keeps it.

Next, we cover short-circuit evaluation, the values that && and || return, default values with ??, safe property access with optional chaining (?.) and the logical assignment shortcuts. At the end, we see how TypeScript uses logical operators to narrow types and to flag conditions that can never change.

1. AND, OR and NOT

The AND operator && is true only when both sides are true, whereas the OR operator || is true when at least one side is true. The NOT operator ! takes one operand and reverses it. For boolean operands, the results follow the classic truth table.

aba && ba || b!a
truetruetruetruefalse
truefalsefalsetruefalse
falsetruefalsetruetrue
falsefalsefalsefalsetrue

When one expression mixes the operators, ! runs first, && second and || last, which is the same order as in Java. Parentheses make the intent visible and change the result when they group differently.

const isAdmin: boolean = true;
const isActive: boolean = false;
const isOwner: boolean = true;

// 1. Combine conditions
const canDelete = isAdmin && isActive;        // canDelete = false
const canView = isAdmin || isActive;          // canView = true
const isBlocked = !isActive;                  // isBlocked = true

// 2. Precedence: ! first, then &&, then ||
const r1 = isOwner || isAdmin && isActive;    // r1 = true
const r2 = (isOwner || isAdmin) && isActive;  // r2 = false

The expression for r1 is read as isOwner || (isAdmin && isActive), so the true value of isOwner decides the result. In r2, the parentheses force the OR to run first.

2. Short-Circuit Evaluation

Both && and || evaluate the left side first and evaluate the right side only when it can still change the result. When the left side of && is falsy, the answer is already known, so the right side never runs, and the same happens for || when the left side is truthy. Skipping the right side is called short-circuit evaluation. For example, a checkout page checks cart && cart.items.length > 0, and the second check never runs when no cart exists yet.

let calls = 0;

// 1. The right side runs only when needed
const a = false && ++calls > 0;               // a = false, calls = 0
const b = true || ++calls > 0;                // b = true, calls = 0
const c = true && ++calls > 0;                // c = true, calls = 1

// 2. Guard against a missing object
const users = [{ name: "Lokesh", age: 37 }];
const raj = users.find((u) => u.name === "Raj");
const rajAge = raj && raj.age;                // rajAge = undefined

The guard in step 2 is the reason short-circuiting matters. The method find() returns undefined when nothing matches, and reading age from undefined throws a TypeError. Because raj is falsy, raj && raj.age stops and returns raj itself. Optional chaining gives the same result with shorter code.

Any function call or side effect on the right side is also skipped, so we never put required work there.

3. What && and || Return

In Java, && and || always produce a boolean. In TypeScript, they return one of the operands. The || operator returns either the first truthy operand or, when none is truthy, the last operand. The && operator returns either the first falsy operand or, when all are truthy, the last operand.

A value is falsy when it counts as false in a condition, and JavaScript has a short list of them, namely false, 0, -0, 0n, “” (empty string), null, undefined and NaN. Every other value is truthy, including “0”, ” ” and empty arrays and objects.

const nickname: string = "";
const fullName: string = "Lokesh Gupta";

// 1. || returns the first truthy operand, or the last one
const shown = nickname || fullName;           // shown = "Lokesh Gupta"

// 2. && returns the first falsy operand, or the last one
const initial = fullName && fullName[0];      // initial = "L"
const empty = nickname && nickname[0];        // empty = ""

// 3. Convert to a real boolean
const hasNickname = !!nickname;               // hasNickname = false
const hasFullName = Boolean(fullName);        // hasFullName = true

The double NOT !! converts any value to its boolean equivalent. The first ! converts and reverses the value, and the second one reverses it back. The function Boolean(value) does the same and reads more clearly. We use one of them when a function’s return type is boolean, because TypeScript will not accept a string there.

4. Nullish Coalescing (??) vs OR (||)

Using || for default values has a known bug, because it also replaces valid values such as 0, “” and false. The nullish coalescing operator ??, added in ES2020 and TypeScript 3.7, returns the right side only when the left side is null or undefined (the two “nullish” values).

Left valuevalue || “default”value ?? “default”
0“default”0
“”“default”“”
false“default”false
NaN“default”NaN
null“default”“default”
undefined“default”“default”

The difference matters for settings, where 0 is often a valid choice. For example, a music player stores the volume a user picked, and 0 means muted.

const settings = new Map<string, number>([["volume", 0]]);

// 1. || drops a valid 0
const volume1 = settings.get("volume") || 50; // volume1 = 50

// 2. ?? keeps it
const volume2 = settings.get("volume") ?? 50; // volume2 = 0
const brightness = settings.get("brightness") ?? 70;   // brightness = 70

// 3. Mixing ?? with || needs parentheses
const nickname: string = "";
const shown = (nickname || null) ?? "Guest";  // shown = "Guest"

The user set the volume to 0, and || changes it to 50 without any error. We use ?? for defaults unless empty strings or 0 should also be replaced. The language does not allow ?? next to && or || without parentheses, so nickname || null ?? “Guest” fails with error TS5076, which says that || and ?? operations cannot be mixed without parentheses.

5. Optional Chaining (?.)

The optional chaining operator ?. reads a property only when the object before it is not null or undefined. Otherwise, the whole expression returns undefined. A user profile page is a typical case, because many users never fill in an address. Optional chaining works for properties (a?.b), array elements (a?.[0]) and method calls (a.greet?.()). Combined with ??, it replaces long chains of && guards.

interface User {
  name: string;
  address?: { city: string };
  tags?: string[];
  greet?: () => string;
}

const lokesh: User = { name: "Lokesh" };
const raj: User = { name: "Raj", address: { city: "Pune" }, tags: ["admin"] };

const city1 = lokesh.address?.city;           // city1 = undefined
const city2 = raj.address?.city;              // city2 = "Pune"
const label = lokesh.address?.city ?? "Unknown";   // label = "Unknown"
const firstTag = raj.tags?.[0];               // firstTag = "admin"
const greeting = lokesh.greet?.();            // greeting = undefined

TypeScript requires the check. Without the ?., the line lokesh.address.city fails to compile with error TS18048 (lokesh.address is possibly undefined), because address is an optional property. The type of city1 is string | undefined, which tells the reader of the code that a missing value is possible.

We use ?. only where a value can be missing. Adding ?. everywhere hides real bugs where an object should always exist.

6. Logical Assignment: &&=, ||= and ??=

The logical assignment operators, added in ES2021 and TypeScript 4.0, combine a logical operator with an assignment. Like && and ||, they short-circuit, so the assignment happens only when it is needed.

OperatorSame asAssigns when the variable is
a &&= ba && (a = b)Truthy
a ||= ba || (a = b)Falsy
a ??= ba ?? (a = b)null or undefined

The most common use of ??= is creating a value on first use, such as a list inside a grouping object. For example, a shop groups its products by category and creates the list for a category when the first product of that category arrives.

// 1. ??= sets a default for null or undefined
let retries: number | undefined;
retries ??= 3;                                // retries = 3

// 2. ||= also replaces 0, "" and false
let title = "";
title ||= "Untitled";                         // title = "Untitled"

// 3. &&= updates only a truthy value
let total = 5;
total &&= total * 2;                          // total = 10

// 4. Create a list on first use
const groups: Record<string, string[]> = {};
groups["fruit"] ??= [];
groups["fruit"].push("apple");
groups["fruit"] ??= [];                       // keeps the existing list
groups["fruit"].push("banana");               // groups = { fruit: ["apple", "banana"] }

The variable keeps its declared type. For let flag = 1, the line flag ||= “x” gives error TS2322, because type string is not assignable to type number. For grouping an existing array in one call, Object.groupBy() and Map.groupBy() from ES2024 are an alternative, which we can use with a TypeScript Map.

7. Type Narrowing and Compiler Checks

TypeScript reads logical operators to narrow types (that is, the compiler uses a check in the code to work out a more specific type for a variable). On the right side of name && name.length, the compiler knows name is truthy, so it is a string and not undefined. In the else branch of if (!name), name is a string as well.

const names: (string | undefined)[] = ["Lokesh", undefined];
const name = names[1];

// 1. && checks name before reading length
const length = name && name.length;           // length = undefined

// 2. ! in an early check
if (!name) {
  console.log("no name");                     // printed
} else {
  console.log(name.toUpperCase());            // name is a string here
}

The type of length is number | “” | undefined, not number, and it includes the empty string because “” is a falsy string that && would return. When we need a plain number, name?.length ?? 0 is the better expression.

Since TypeScript 5.6, the compiler also reports conditions whose result is fixed by the syntax. A condition with a fixed result points to a typo or a missing variable in most cases.

const label = "" || "Guest";
const options = {} || { debug: true };
const count = 0 ?? 5;
src/err.ts(1,15): error TS2873: This kind of expression is always falsy.
src/err.ts(2,17): error TS2872: This kind of expression is always truthy.
src/err.ts(3,15): error TS2869: Right operand of ?? is unreachable because the left operand is never nullish.

An object literal is always truthy and the number 0 is never nullish, so the right side of the second and third lines can never matter. The TypeScript 5.6 checks look at the written expression, not at the declared type. For that reason, count ?? 10 compiles in the first example, where count is a variable of type number.

8. Running the Code Locally

The logical-operators folder on GitHub has one file per section and prints each value from the snippet comments, including the || vs ?? table from section 4. Node.js 22 is the minimum version, and npm install fetches TypeScript 7.0.2. We left the snippets marked as not compiling out of the project.

npm install
npm start

9. Conclusion

TypeScript logical operators short-circuit and return one of their operands, which makes && useful for guards and || for fallbacks. For default values, ?? is safer than || because it keeps 0, “” and false.

Optional chaining ?. reads values that may be missing, and the logical assignment operators assign only when needed. On top of the runtime rules, the compiler narrows types after each check and reports conditions whose result can never change. In most code, the values that logical operators combine come from comparison operators.

10. References

MDN covers the runtime behavior of each operator, and the TypeScript release notes list the version that introduced each compiler feature.

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.