JavaScript Tutorial

JavaScript Special Operators: Complete Guide with Examples

Master JavaScript special operators including ternary (?:), typeof, instanceof, void, delete, in, comma, optional chaining (?.), and spread (...). Learn with practical examples and best practices.

Welcome back! 👋 In the previous lessons, you learned about various operators. Now, let's explore special operators - a collection of unique operators that serve specific purposes in JavaScript.

These operators don't fit neatly into other categories but are incredibly useful for writing concise, expressive code. Let's master them!


What are Special Operators?

Special operators are unique operators that perform specialized tasks in JavaScript. They include:

  1. Ternary Operator (? :) - Conditional expression
  2. typeof Operator - Type checking
  3. instanceof Operator - Object type checking
  4. void Operator - Returns undefined
  5. delete Operator - Delete object properties
  6. in Operator - Check property existence
  7. Comma Operator (,) - Evaluate multiple expressions
  8. Optional Chaining (?.) - Safe property access (ES2020)
  9. Spread Operator (...) - Expand iterables (ES6)
  10. Rest Operator (...) - Collect arguments (ES6)

Ternary Operator (? :)

The ternary operator (also called conditional operator) is a shorthand for if-else statements. It's the only JavaScript operator that takes three operands.

Syntax

condition ? expressionIfTrue : expressionIfFalse

Basic Examples

// Simple condition
let age = 20;
let status = age >= 18 ? "adult" : "minor";
console.log(status); // "adult"

// Equivalent if-else
let status2;
if (age >= 18) {
  status2 = "adult";
} else {
  status2 = "minor";
}

// Inline usage
console.log(age >= 18 ? "Can vote" : "Cannot vote"); // "Can vote"

// With variables
let temperature = 25;
let weather = temperature > 30 ? "hot" : "comfortable";
console.log(weather); // "comfortable"

// With numbers
let a = 10;
let b = 20;
let max = a > b ? a : b;
console.log(max); // 20

Nested Ternary Operators

You can nest ternary operators for multiple conditions:

// Grade calculator
let score = 85;
let grade = score >= 90 ? "A" :
            score >= 80 ? "B" :
            score >= 70 ? "C" :
            score >= 60 ? "D" : "F";
console.log(grade); // "B"

// Equivalent if-else chain
let grade2;
if (score >= 90) {
  grade2 = "A";
} else if (score >= 80) {
  grade2 = "B";
} else if (score >= 70) {
  grade2 = "C";
} else if (score >= 60) {
  grade2 = "D";
} else {
  grade2 = "F";
}

// Time of day greeting
let hour = 14;
let greeting = hour < 12 ? "Good morning" :
               hour < 18 ? "Good afternoon" :
               "Good evening";
console.log(greeting); // "Good afternoon"

⚠️ Warning: Nested ternaries can become hard to read. Use sparingly!

Ternary with Function Calls

let isLoggedIn = true;
let message = isLoggedIn ? showDashboard() : showLogin();

function showDashboard() {
  return "Welcome to your dashboard!";
}

function showLogin() {
  return "Please log in";
}

console.log(message);

Practical Examples

// Discount calculator
function calculatePrice(price, isMember) {
  return isMember ? price * 0.9 : price;
}

console.log(calculatePrice(100, true));  // 90
console.log(calculatePrice(100, false)); // 100

// Button text
let isFollowing = false;
let buttonText = isFollowing ? "Unfollow" : "Follow";
console.log(buttonText); // "Follow"

// Pluralization
function formatItems(count) {
  return `${count} item${count !== 1 ? 's' : ''}`;
}

console.log(formatItems(0));  // "0 items"
console.log(formatItems(1));  // "1 item"
console.log(formatItems(5));  // "5 items"

// Default value with condition
let userInput = "";
let displayValue = userInput !== "" ? userInput : "Default";
console.log(displayValue); // "Default"

// Conditional CSS class
let isActive = true;
let className = `button ${isActive ? 'button--active' : 'button--inactive'}`;
console.log(className); // "button button--active"

// Minimum/Maximum
let x = 15;
let y = 20;
let min = x < y ? x : y;  // 15
let max = x > y ? x : y;  // 20

Best Practices

// ❌ Hard to read - too nested
let result = a ? b ? c ? d : e : f : g;

// ✅ Use if-else for complex logic
let result;
if (a) {
  if (b) {
    result = c ? d : e;
  } else {
    result = f;
  }
} else {
  result = g;
}

// ❌ Side effects in ternary (confusing)
let value = condition ? (x++, y) : (z--, w);

// ✅ Keep ternary simple and pure
let value = condition ? y : w;
if (condition) {
  x++;
} else {
  z--;
}

// ✅ Good use - simple condition
let access = isAdmin ? "full" : "limited";

// ✅ Good use - inline rendering logic
console.log(`You have ${count} ${count === 1 ? 'item' : 'items'}`);

typeof Operator

The typeof operator returns a string indicating the type of a value.

Syntax

typeof operand
// or
typeof(operand)

Return Values

// Primitives
console.log(typeof 42);           // "number"
console.log(typeof 3.14);         // "number"
console.log(typeof NaN);          // "number" (surprising!)
console.log(typeof Infinity);     // "number"

console.log(typeof "hello");      // "string"
console.log(typeof "");           // "string"

console.log(typeof true);         // "boolean"
console.log(typeof false);        // "boolean"

console.log(typeof undefined);    // "undefined"

console.log(typeof Symbol());     // "symbol"
console.log(typeof 123n);         // "bigint"

// Objects and special cases
console.log(typeof {});           // "object"
console.log(typeof []);           // "object" ⚠️
console.log(typeof null);         // "object" ⚠️ (JavaScript bug!)
console.log(typeof function(){}); // "function"
console.log(typeof class {});     // "function"
console.log(typeof new Date());   // "object"
console.log(typeof /regex/);      // "object"

Common Gotchas

// ⚠️ typeof null is "object" (historical bug)
console.log(typeof null); // "object"

// Check for null explicitly
let value = null;
if (value === null) {
  console.log("Value is null");
}

// ⚠️ typeof array is "object"
console.log(typeof [1, 2, 3]); // "object"

// Use Array.isArray() instead
console.log(Array.isArray([1, 2, 3])); // true

// ⚠️ typeof NaN is "number"
console.log(typeof NaN); // "number"

// Use Number.isNaN() to check
console.log(Number.isNaN(NaN)); // true

Practical Examples

// Type validation
function validateInput(value, expectedType) {
  if (typeof value !== expectedType) {
    throw new TypeError(`Expected ${expectedType}, got ${typeof value}`);
  }
  return true;
}

validateInput(42, "number");      // ✅
// validateInput("hello", "number"); // ❌ TypeError

// Check if variable is defined
if (typeof myVariable !== "undefined") {
  console.log("Variable is defined");
}

// Safe feature detection
if (typeof window !== "undefined") {
  console.log("Running in browser");
}

if (typeof process !== "undefined") {
  console.log("Running in Node.js");
}

// Dynamic type checking
function processValue(value) {
  if (typeof value === "string") {
    return value.toUpperCase();
  } else if (typeof value === "number") {
    return value * 2;
  } else if (typeof value === "boolean") {
    return !value;
  }
  return value;
}

console.log(processValue("hello")); // "HELLO"
console.log(processValue(5));       // 10
console.log(processValue(true));    // false

// Type guard function
function isString(value) {
  return typeof value === "string";
}

function isNumber(value) {
  return typeof value === "number" && !isNaN(value);
}

function isFunction(value) {
  return typeof value === "function";
}

console.log(isString("hello"));     // true
console.log(isNumber(42));          // true
console.log(isFunction(() => {}));  // true

instanceof Operator

The instanceof operator tests whether an object is an instance of a specific constructor or class.

Syntax

object instanceof Constructor

Basic Examples

// Arrays
let arr = [1, 2, 3];
console.log(arr instanceof Array);   // true
console.log(arr instanceof Object);  // true (arrays are objects)

// Objects
let obj = { name: "John" };
console.log(obj instanceof Object);  // true
console.log(obj instanceof Array);   // false

// Dates
let date = new Date();
console.log(date instanceof Date);   // true
console.log(date instanceof Object); // true

// RegExp
let regex = /test/;
console.log(regex instanceof RegExp); // true

// Functions
function myFunc() {}
console.log(myFunc instanceof Function); // true
console.log(myFunc instanceof Object);   // true

// Primitives (not instances)
console.log(5 instanceof Number);        // false
console.log("hello" instanceof String);  // false
console.log(true instanceof Boolean);    // false

// But wrapper objects are instances
console.log(new Number(5) instanceof Number);     // true
console.log(new String("hello") instanceof String); // true

With Custom Classes

class Animal {
  constructor(name) {
    this.name = name;
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }
}

let animal = new Animal("Generic");
let dog = new Dog("Buddy", "Golden Retriever");

console.log(animal instanceof Animal); // true
console.log(animal instanceof Dog);    // false

console.log(dog instanceof Dog);       // true
console.log(dog instanceof Animal);    // true (inheritance)
console.log(dog instanceof Object);    // true (everything is an object)

// Plain object is not an instance of Animal
let plainObj = { name: "Not an animal" };
console.log(plainObj instanceof Animal); // false

Practical Examples

// Type checking in functions
function processAnimal(animal) {
  if (!(animal instanceof Animal)) {
    throw new TypeError("Expected Animal instance");
  }
  console.log(`Processing ${animal.name}`);
}

// Error handling
try {
  throw new Error("Something went wrong");
} catch (e) {
  if (e instanceof Error) {
    console.log("Error message:", e.message);
  } else {
    console.log("Unknown error:", e);
  }
}

// Custom error types
class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = "ValidationError";
  }
}

try {
  throw new ValidationError("Invalid input");
} catch (e) {
  if (e instanceof ValidationError) {
    console.log("Validation error:", e.message);
  } else if (e instanceof Error) {
    console.log("General error:", e.message);
  }
}

// Polymorphism
function makeSound(animal) {
  if (animal instanceof Dog) {
    return "Woof!";
  } else if (animal instanceof Animal) {
    return "Generic animal sound";
  }
  return "Not an animal";
}

// Array vs Object detection
function getType(value) {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  if (value instanceof Date) return "date";
  if (value instanceof RegExp) return "regexp";
  return typeof value;
}

console.log(getType([]));           // "array"
console.log(getType({}));           // "object"
console.log(getType(null));         // "null"
console.log(getType(new Date()));   // "date"

instanceof vs typeof

let arr = [1, 2, 3];

// typeof returns "object" for arrays
console.log(typeof arr);          // "object"

// instanceof correctly identifies arrays
console.log(arr instanceof Array); // true

// Combined approach
function isArray(value) {
  // Modern way
  return Array.isArray(value);
  
  // Or using instanceof
  // return value instanceof Array;
}

// Type checking function
function getDetailedType(value) {
  if (value === null) return "null";
  if (value === undefined) return "undefined";
  if (Array.isArray(value)) return "array";
  if (value instanceof Date) return "date";
  if (value instanceof RegExp) return "regexp";
  if (value instanceof Error) return "error";
  return typeof value;
}

console.log(getDetailedType(null));       // "null"
console.log(getDetailedType([]));         // "array"
console.log(getDetailedType(new Date())); // "date"
console.log(getDetailedType(42));         // "number"

void Operator

The void operator evaluates an expression and returns undefined.

Syntax

void expression

Basic Examples

console.log(void 0);           // undefined
console.log(void 1);           // undefined
console.log(void "hello");     // undefined
console.log(void (2 + 3));     // undefined (expression evaluated, but returns undefined)

// Comparing
console.log(undefined);        // undefined
console.log(void 0);           // undefined (same result)

Practical Use Cases

1. Ensure Undefined:

// Old way to get undefined (before it was guaranteed)
let myUndefined = void 0;

// Modern use: Ensure you get undefined
function doSomething() {
  return void 0;  // Explicitly return undefined
}

console.log(doSomething() === undefined); // true

2. Prevent Page Navigation:

<!-- Prevent default anchor behavior -->
<a href="javascript:void(0)" onclick="doSomething()">Click me</a>

<!-- Better modern approach: use preventDefault -->
<a href="#" onclick="event.preventDefault(); doSomething()">Click me</a>

3. IIFE (Immediately Invoked Function Expression):

// Using void to invoke function
void function() {
  console.log("This runs immediately");
}();

// More common: parentheses syntax
(function() {
  console.log("This also runs immediately");
})();

4. Arrow Functions with Side Effects:

// Ensure function returns undefined (not the side effect result)
let numbers = [1, 2, 3];

// ❌ Returns array of new lengths
let result1 = numbers.map(n => numbers.push(n * 2));
console.log(result1); // [4, 5, 6] (confusing!)

// ✅ Using void to ignore return value
let result2 = numbers.map(n => void numbers.push(n * 2));
console.log(result2); // [undefined, undefined, undefined]

// ✅ Better: use forEach for side effects
numbers.forEach(n => numbers.push(n * 2));

Note: void is rarely used in modern JavaScript. It's mostly seen in legacy code.


delete Operator

The delete operator removes a property from an object.

Syntax

delete object.property
// or
delete object['property']

Basic Examples

let person = {
  name: "John",
  age: 30,
  city: "New York"
};

console.log(person); // { name: "John", age: 30, city: "New York" }

// Delete a property
delete person.age;
console.log(person); // { name: "John", city: "New York" }

// Property no longer exists
console.log(person.age); // undefined

// Check if property exists
console.log("age" in person); // false

// Delete with bracket notation
delete person["city"];
console.log(person); // { name: "John" }

Return Value

let obj = { x: 1 };

// Returns true if deletion was successful
console.log(delete obj.x);  // true
console.log(obj);           // {}

// Returns true even if property doesn't exist
console.log(delete obj.y);  // true

// Returns false for non-configurable properties
console.log(delete Object.prototype); // false (can't delete)

What Can and Cannot Be Deleted

// ✅ Can delete: Object properties
let obj = { a: 1, b: 2 };
delete obj.a;  // ✅

// ✅ Can delete: Array elements (but leaves hole)
let arr = [1, 2, 3];
delete arr[1];
console.log(arr);        // [1, empty, 3]
console.log(arr.length); // 3 (length unchanged!)
console.log(arr[1]);     // undefined

// ❌ Cannot delete: Variables
let x = 5;
delete x;  // false (does nothing in strict mode, error in strict mode)
console.log(x); // 5

// ❌ Cannot delete: Function parameters
function test(param) {
  delete param;  // false
  console.log(param); // Still exists
}

// ❌ Cannot delete: Built-in properties
delete Math.PI;  // false
console.log(Math.PI); // 3.14159... (still exists)

// ❌ Cannot delete: Array length
let arr2 = [1, 2, 3];
delete arr2.length;  // false
console.log(arr2.length); // 3

Practical Examples

// Remove sensitive data
let user = {
  name: "John",
  email: "john@example.com",
  password: "secret123",
  creditCard: "1234-5678-9012-3456"
};

function sanitizeUser(user) {
  let copy = { ...user };
  delete copy.password;
  delete copy.creditCard;
  return copy;
}

let safeUser = sanitizeUser(user);
console.log(safeUser);
// { name: "John", email: "john@example.com" }

// Dynamic property removal
function removeProperties(obj, ...keys) {
  let result = { ...obj };
  keys.forEach(key => delete result[key]);
  return result;
}

let data = { a: 1, b: 2, c: 3, d: 4 };
let filtered = removeProperties(data, 'b', 'd');
console.log(filtered); // { a: 1, c: 3 }

// Clean up cache
let cache = {
  "user_123": { name: "John" },
  "user_456": { name: "Jane" },
  "user_789": { name: "Bob" }
};

function removeFromCache(id) {
  delete cache[`user_${id}`];
}

removeFromCache(123);
console.log(cache);
// { user_456: {...}, user_789: {...} }

// Remove undefined properties
function removeUndefined(obj) {
  Object.keys(obj).forEach(key => {
    if (obj[key] === undefined) {
      delete obj[key];
    }
  });
  return obj;
}

let data2 = { a: 1, b: undefined, c: 3, d: undefined };
removeUndefined(data2);
console.log(data2); // { a: 1, c: 3 }

delete vs Setting to undefined

let obj = { a: 1, b: 2 };

// Using delete
delete obj.a;
console.log("a" in obj);        // false (property removed)
console.log(Object.keys(obj));  // ["b"]

// Setting to undefined
obj.b = undefined;
console.log("b" in obj);        // true (property still exists!)
console.log(Object.keys(obj));  // ["b"]
console.log(obj.b);             // undefined

// delete actually removes the property
// Setting to undefined just changes the value

in Operator

The in operator checks if a property exists in an object or its prototype chain.

Syntax

property in object

Basic Examples

let person = {
  name: "John",
  age: 30
};

console.log("name" in person);   // true
console.log("age" in person);    // true
console.log("email" in person);  // false

// Works with arrays
let arr = [1, 2, 3];
console.log(0 in arr);    // true (index 0 exists)
console.log(1 in arr);    // true
console.log(5 in arr);    // false (index 5 doesn't exist)
console.log("length" in arr); // true (length property exists)

// Checks prototype chain too
console.log("toString" in person); // true (inherited from Object)
console.log("hasOwnProperty" in person); // true (inherited)

in vs hasOwnProperty

let obj = { a: 1 };

// `in` checks own properties AND prototype chain
console.log("a" in obj);         // true (own property)
console.log("toString" in obj);  // true (from prototype)

// hasOwnProperty checks ONLY own properties
console.log(obj.hasOwnProperty("a"));        // true
console.log(obj.hasOwnProperty("toString")); // false (not own)

// Better modern way
console.log(Object.hasOwn(obj, "a"));        // true
console.log(Object.hasOwn(obj, "toString")); // false

Practical Examples

// Check for property before accessing
function getUserEmail(user) {
  if ("email" in user) {
    return user.email;
  }
  return "No email provided";
}

let user1 = { name: "John", email: "john@example.com" };
let user2 = { name: "Jane" };

console.log(getUserEmail(user1)); // "john@example.com"
console.log(getUserEmail(user2)); // "No email provided"

// Validate required fields
function validateObject(obj, requiredFields) {
  let missing = [];
  
  for (let field of requiredFields) {
    if (!(field in obj)) {
      missing.push(field);
    }
  }
  
  return {
    valid: missing.length === 0,
    missing: missing
  };
}

let data = { name: "John", age: 30 };
let result = validateObject(data, ["name", "age", "email"]);

console.log(result);
// { valid: false, missing: ["email"] }

// Check for method existence (duck typing)
function canFly(obj) {
  return "fly" in obj && typeof obj.fly === "function";
}

let bird = {
  fly() { console.log("Flying!"); }
};

let dog = {
  bark() { console.log("Barking!"); }
};

console.log(canFly(bird)); // true
console.log(canFly(dog));  // false

// Iterate over object properties
let config = {
  host: "localhost",
  port: 3000,
  debug: true
};

for (let key in config) {
  if (key in config) {  // Always true in for...in
    console.log(`${key}: ${config[key]}`);
  }
}

// Check for optional chaining support
if ("?." in { a: { b: 1 } }) {
  console.log("Optional chaining syntax exists");
}

in with Arrays

let arr = [10, 20, 30];

// Check index
console.log(0 in arr);  // true
console.log(2 in arr);  // true
console.log(5 in arr);  // false

// After delete (creates hole)
delete arr[1];
console.log(1 in arr);  // false (hole in array)
console.log(arr);       // [10, empty, 30]

// vs checking value
console.log(arr[1] === undefined);  // true (but misleading)
console.log(1 in arr);              // false (more accurate)

// Sparse array
let sparse = [];
sparse[0] = "a";
sparse[5] = "b";

console.log(0 in sparse); // true
console.log(1 in sparse); // false (empty)
console.log(5 in sparse); // true

Comma Operator (,)

The comma operator evaluates each operand from left to right and returns the value of the last operand.

Syntax

expression1, expression2, expression3, ...

Basic Examples

// Returns last value
let x = (1, 2, 3);
console.log(x); // 3

// Multiple expressions
let a = (5, 10, 15, 20);
console.log(a); // 20

// With side effects
let count = 0;
let result = (count++, count++, count++, count);
console.log(result); // 3
console.log(count);  // 3

// In variable declaration (not recommended)
let x2 = 1, y = 2, z = 3;
console.log(x2, y, z); // 1 2 3

Common Use Cases

1. For Loops (Most Common):

// Multiple updates
for (let i = 0, j = 10; i < 5; i++, j--) {
  console.log(`i: ${i}, j: ${j}`);
}
// i: 0, j: 10
// i: 1, j: 9
// i: 2, j: 8
// i: 3, j: 7
// i: 4, j: 6

// Multiple initializations
for (let i = 0, len = arr.length, sum = 0; i < len; i++) {
  sum += arr[i];
}

2. Compact Code (Use Sparingly):

// Swap variables
let a = 1, b = 2;
a = (b = a + b) - (a = b);
console.log(a, b); // 2 1

// Better way:
[a, b] = [b, a]; // ✅ Clear destructuring

3. Return Multiple Operations:

function updateCounter() {
  return (counter++, log(), counter); // Do multiple things, return last
}

Why Comma Operator is Rarely Used

// ❌ Confusing and hard to read
let result = (doThis(), doThat(), getFinalValue());

// ✅ Much clearer
doThis();
doThat();
let result = getFinalValue();

// ❌ Clever but unclear
let x = (y = 5, z = 10, y + z);

// ✅ Clear and explicit
let y = 5;
let z = 10;
let x = y + z;

Note: The comma operator is considered a code smell in most cases. Use it only in for loops where it's conventional.


Optional Chaining (?.)

The optional chaining operator allows safe access to nested properties without checking each level for null/undefined.

Syntax

obj?.property
obj?.[expression]
obj?.method()

Introduced in ES2020

Basic Examples

let user = {
  name: "John",
  address: {
    street: "123 Main St",
    city: "New York"
  }
};

// Without optional chaining
let city1 = user && user.address && user.address.city;

// With optional chaining
let city2 = user?.address?.city;
console.log(city2); // "New York"

// If property doesn't exist
let country = user?.address?.country;
console.log(country); // undefined (no error!)

// With null user
let nullUser = null;
let name = nullUser?.name;
console.log(name); // undefined (doesn't throw error)

Avoiding Errors

// ❌ Without optional chaining (throws error)
let user1 = null;
// console.log(user1.address.city); // TypeError: Cannot read property 'address' of null

// ✅ With optional chaining (returns undefined)
console.log(user1?.address?.city); // undefined (safe!)

// Deeply nested
let data = {
  users: [
    {
      profile: {
        settings: {
          theme: "dark"
        }
      }
    }
  ]
};

// Safe access
let theme = data?.users?.[0]?.profile?.settings?.theme;
console.log(theme); // "dark"

// If any level is missing
let missing = data?.users?.[5]?.profile?.settings?.theme;
console.log(missing); // undefined (no error)

With Methods

let user = {
  name: "John",
  greet() {
    return `Hello, ${this.name}!`;
  }
};

// Call method if it exists
console.log(user.greet?.()); // "Hello, John!"

// If method doesn't exist
console.log(user.goodbye?.()); // undefined (doesn't throw)

// With null object
let nullUser = null;
console.log(nullUser?.greet?.()); // undefined (safe)

With Array Access

let users = [
  { name: "John" },
  { name: "Jane" }
];

// Safe array access
console.log(users?.[0]?.name);  // "John"
console.log(users?.[5]?.name);  // undefined (no error)

// With potentially undefined array
let emptyArray = null;
console.log(emptyArray?.[0]?.name); // undefined (safe)

Practical Examples

// API response handling
function getUserCity(apiResponse) {
  return apiResponse?.data?.user?.address?.city ?? "Unknown";
}

let response1 = {
  data: {
    user: {
      address: {
        city: "New York"
      }
    }
  }
};

let response2 = { data: {} };
let response3 = null;

console.log(getUserCity(response1)); // "New York"
console.log(getUserCity(response2)); // "Unknown"
console.log(getUserCity(response3)); // "Unknown"

// Event handling
function handleClick(event) {
  let targetId = event?.target?.id;
  
  if (targetId) {
    console.log(`Clicked element with ID: ${targetId}`);
  }
}

// Configuration access
function getConfigValue(config, key) {
  return config?.settings?.[key] ?? "default";
}

// DOM element access
let element = document.querySelector("#myElement");
let textContent = element?.textContent;
let firstChild = element?.firstChild?.nodeName;

// Callback execution
function executeCallback(obj) {
  obj?.onSuccess?.();  // Only calls if onSuccess exists
}

// Deep object access with fallback
let user = {
  profile: {
    preferences: {
      notifications: {
        email: true
      }
    }
  }
};

let emailNotif = user?.profile?.preferences?.notifications?.email ?? false;

Optional Chaining vs AND (&&)

let obj = { value: 0 };

// Using &&
console.log(obj && obj.value);  // 0 ✅

// Using ?.
console.log(obj?.value);        // 0 ✅

// Difference with falsy values
let data = { count: 0 };

// && stops at first falsy
console.log(data && data.count && data.count.toString()); // 0 (stops)

// ?. only checks null/undefined
console.log(data?.count?.toString()); // "0" ✅

Short-Circuiting

let count = 0;

// Short-circuits if left side is null/undefined
let result = null?.property;
// Nothing after ?. is evaluated

let obj = null;
obj?.method(); // method is not called
count++;
console.log(count); // 1 (count++ still executes)

Spread Operator (...)

The spread operator expands an iterable (array, string, object) into individual elements.

Syntax

// In arrays
[...iterable]

// In objects
{...object}

// In function calls
func(...iterable)

Introduced in ES6 (ES2015)

Array Spreading

// Expand array
let arr1 = [1, 2, 3];
let arr2 = [...arr1, 4, 5];
console.log(arr2); // [1, 2, 3, 4, 5]

// Concatenate arrays
let a = [1, 2];
let b = [3, 4];
let combined = [...a, ...b];
console.log(combined); // [1, 2, 3, 4]

// Clone array (shallow copy)
let original = [1, 2, 3];
let copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] (unchanged)
console.log(copy);     // [1, 2, 3, 4]

// Insert in middle
let arr = [1, 2, 5, 6];
let inserted = [1, 2, ...[ 3, 4], 5, 6];
console.log(inserted); // [1, 2, 3, 4, 5, 6]

// String to array
let str = "hello";
let chars = [...str];
console.log(chars); // ["h", "e", "l", "l", "o"]

Object Spreading

// Clone object (shallow copy)
let user = { name: "John", age: 30 };
let userCopy = { ...user };
console.log(userCopy); // { name: "John", age: 30 }

// Merge objects
let defaults = { theme: "light", lang: "en" };
let userPrefs = { theme: "dark" };
let config = { ...defaults, ...userPrefs };
console.log(config); // { theme: "dark", lang: "en" }

// Add properties
let person = { name: "John" };
let employee = { ...person, role: "Developer", salary: 75000 };
console.log(employee);
// { name: "John", role: "Developer", salary: 75000 }

// Override properties (order matters)
let obj1 = { a: 1, b: 2 };
let obj2 = { ...obj1, b: 3, c: 4 };
console.log(obj2); // { a: 1, b: 3, c: 4 }

// Nested objects (shallow copy gotcha)
let user1 = {
  name: "John",
  address: { city: "NYC" }
};
let user2 = { ...user1 };
user2.address.city = "LA";
console.log(user1.address.city); // "LA" ⚠️ (reference copied!)

Function Arguments

// Spread in function calls
function sum(a, b, c) {
  return a + b + c;
}

let numbers = [1, 2, 3];
console.log(sum(...numbers)); // 6

// Math functions
let nums = [5, 10, 2, 8, 3];
console.log(Math.max(...nums));  // 10
console.log(Math.min(...nums));  // 2

// Multiple spreads
let arr1 = [1, 2];
let arr2 = [3, 4];
console.log(sum(...arr1, ...arr2)); // TypeError (too many args)

// Combine with regular args
function greet(greeting, ...names) {
  return `${greeting}, ${names.join(" and ")}!`;
}
console.log(greet("Hello", "John", "Jane")); // "Hello, John and Jane!"

Practical Examples

// Remove duplicates
let arr = [1, 2, 2, 3, 3, 4];
let unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]

// Merge and deduplicate
let arr1 = [1, 2, 3];
let arr2 = [3, 4, 5];
let merged = [...new Set([...arr1, ...arr2])];
console.log(merged); // [1, 2, 3, 4, 5]

// Convert NodeList to array
let divs = document.querySelectorAll('div');
let divArray = [...divs];
divArray.forEach(div => console.log(div));

// Shallow clone with modifications
let product = {
  id: 1,
  name: "Laptop",
  price: 999
};

let discountedProduct = {
  ...product,
  price: product.price * 0.9,
  onSale: true
};
console.log(discountedProduct);
// { id: 1, name: "Laptop", price: 899.1, onSale: true }

// Conditionally add properties
let isAdmin = true;
let user = {
  name: "John",
  ...(isAdmin && { role: "admin" })
};
console.log(user);
// { name: "John", role: "admin" }

// Update nested object (immutable)
let state = {
  user: {
    name: "John",
    age: 30
  }
};

let newState = {
  ...state,
  user: {
    ...state.user,
    age: 31
  }
};
console.log(newState.user.age); // 31
console.log(state.user.age);    // 30 (original unchanged)

Rest Operator (...)

The rest operator collects multiple elements into an array. It looks the same as spread but is used differently.

Syntax

// In function parameters
function func(...args)

// In destructuring
let [first, ...rest] = array
let {prop, ...rest} = object

Function Parameters (Rest Parameters)

// Collect all arguments
function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3));       // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

// Mix with regular parameters
function greet(greeting, ...names) {
  return `${greeting}, ${names.join(" and ")}!`;
}

console.log(greet("Hello", "John"));           // "Hello, John!"
console.log(greet("Hi", "Alice", "Bob"));      // "Hi, Alice and Bob!"
console.log(greet("Hey", "A", "B", "C"));      // "Hey, A and B and C!"

// Rest must be last parameter
function wrong(a, ...rest, b) { } // ❌ SyntaxError
function correct(a, b, ...rest) { } // ✅

// Access with old-style arguments
function oldWay() {
  console.log(arguments); // Arguments object (array-like)
}

function newWay(...args) {
  console.log(args); // Actual array
}

Array Destructuring

// Get first and rest
let numbers = [1, 2, 3, 4, 5];
let [first, ...rest] = numbers;

console.log(first); // 1
console.log(rest);  // [2, 3, 4, 5]

// Get first, second, and rest
let [a, b, ...others] = [10, 20, 30, 40, 50];
console.log(a);      // 10
console.log(b);      // 20
console.log(others); // [30, 40, 50]

// Skip elements
let [, , ...remaining] = [1, 2, 3, 4, 5];
console.log(remaining); // [3, 4, 5]

// Rest in nested destructuring
let data = [1, [2, 3, 4]];
let [first2, [second, ...innerRest]] = data;
console.log(first2);    // 1
console.log(second);    // 2
console.log(innerRest); // [3, 4]

Object Destructuring

// Extract specific properties, collect rest
let user = {
  name: "John",
  age: 30,
  city: "NYC",
  country: "USA"
};

let { name, ...otherInfo } = user;
console.log(name);      // "John"
console.log(otherInfo); // { age: 30, city: "NYC", country: "USA" }

// Remove properties
let { password, creditCard, ...safeUser } = {
  name: "John",
  password: "secret",
  creditCard: "1234",
  email: "john@example.com"
};
console.log(safeUser); // { name: "John", email: "john@example.com" }

// Nested rest
let data2 = {
  id: 1,
  user: {
    name: "John",
    age: 30,
    email: "john@example.com"
  }
};

let { user: { name: userName, ...userRest }, ...dataRest } = data2;
console.log(userName);  // "John"
console.log(userRest);  // { age: 30, email: "john@example.com" }
console.log(dataRest);  // { id: 1 }

Practical Examples

// Variable argument function
function createLogger(level, ...messages) {
  let timestamp = new Date().toISOString();
  console.log(`[${timestamp}] [${level}]:`, ...messages);
}

createLogger("INFO", "Server started");
createLogger("ERROR", "Connection failed", "Retrying...");

// Combine with default values
function fetchData(url, { method = "GET", ...options } = {}) {
  console.log("URL:", url);
  console.log("Method:", method);
  console.log("Other options:", options);
}

fetchData("/api/users", { method: "POST", body: {}, headers: {} });

// Remove properties from object
function omit(obj, ...keys) {
  let copy = { ...obj };
  keys.forEach(key => delete copy[key]);
  return copy;
}

let user2 = { name: "John", age: 30, password: "secret" };
let safe = omit(user2, "password");
console.log(safe); // { name: "John", age: 30 }

// Group by first argument
function groupLog(category, ...items) {
  console.log(`=== ${category} ===`);
  items.forEach(item => console.log(`  - ${item}`));
}

groupLog("Fruits", "Apple", "Banana", "Orange");
// === Fruits ===
//   - Apple
//   - Banana
//   - Orange

// Curry with rest
function multiply(multiplier) {
  return (...numbers) => numbers.map(n => n * multiplier);
}

let double = multiply(2);
console.log(double(1, 2, 3, 4)); // [2, 4, 6, 8]

Summary

Congratulations! 🎉 You now have a comprehensive understanding of JavaScript's special operators.

Key Takeaways

Ternary Operator (? :)

  • Shorthand for if-else
  • Keep simple, avoid deep nesting
  • condition ? true : false

typeof Operator

  • Returns type as string
  • Watch for "object" (arrays, null)
  • Use for primitive type checking

instanceof Operator

  • Check object instances
  • Works with inheritance
  • Use for object type checking

void Operator

  • Always returns undefined
  • Rarely used in modern JS
  • Historical use in IIFE and links

delete Operator

  • Remove object properties
  • Cannot delete variables
  • Returns true/false

in Operator

  • Check property existence
  • Includes prototype chain
  • Use with hasOwnProperty for own properties

Comma Operator (,)

  • Evaluates left to right
  • Returns last value
  • Mainly used in for loops

Optional Chaining (?.)

  • Safe nested access
  • Prevents null/undefined errors
  • Modern and essential (ES2020)

Spread Operator (...)

  • Expand iterables
  • Clone arrays/objects (shallow)
  • Merge and combine

Rest Operator (...)

  • Collect into array
  • Function parameters
  • Destructuring remainder

What's Next?

Excellent work! You now understand all the special operators in JavaScript.

In the next lesson, we'll explore Operator Precedence where you'll learn:

  • Complete precedence table
  • Associativity (left-to-right vs right-to-left)
  • How to read complex expressions
  • When to use parentheses
  • Common precedence mistakes

This will tie together everything you've learned about operators!