Set Operations in JavaScript: From Math Symbols to Code#

I used to write nested loops when comparing two lists—finding common items, merging with deduplication, calculating differences. The code was messy. Then I discovered JavaScript’s Set object, which is practically built for this.

Mathematical Foundation#

Quick refresher:

  • Union (A ∪ B): All elements in A or B
  • Intersection (A ∩ B): Elements in both A and B
  • Difference (A - B): Elements in A but not in B
  • Symmetric Difference (A △ B): Elements in either A or B, but not both

Mathematically, set elements are unique and unordered. JavaScript’s Set matches this exactly.

Implementing Five Operations#

Union: Spread Operator Magic#

function union<T>(a: Set<T>, b: Set<T>): Set<T> {
  return new Set([...a, ...b])
}

// Example
const setA = new Set([1, 2, 3])
const setB = new Set([3, 4, 5])
console.log(union(setA, setB))  // Set {1, 2, 3, 4, 5}

How it works: [...a] spreads the Set into an array, two arrays merge, and constructing a new Set auto-deduplicates.

Intersection: filter + has#

function intersection<T>(a: Set<T>, b: Set<T>): Set<T> {
  return new Set([...a].filter(x => b.has(x)))
}

console.log(intersection(setA, setB))  // Set {3}

Iterate through A, keep elements that exist in B. Set.has() is O(1)—much faster than array’s includes().

Difference: Reverse Filter#

function difference<T>(a: Set<T>, b: Set<T>): Set<T> {
  return new Set([...a].filter(x => !b.has(x)))
}

console.log(difference(setA, setB))  // Set {1, 2}

Just one ! different from intersection—keep elements NOT in B.

Symmetric Difference: Union of Two Differences#

function symmetricDifference<T>(a: Set<T>, b: Set<T>): Set<T> {
  const diffAB = new Set([...a].filter(x => !b.has(x)))
  const diffBA = new Set([...b].filter(x => !a.has(x)))
  return new Set([...diffAB, ...diffBA])
}

console.log(symmetricDifference(setA, setB))  // Set {1, 2, 4, 5}

You could also think of it as (A ∪ B) - (A ∩ B), but two differences + union is more straightforward.

Case-Insensitive Comparison#

Real-world data often has inconsistent casing. Compare in lowercase, but preserve original form in output:

function intersectionCaseInsensitive(a: string[], b: string[]): string[] {
  const bLower = new Set(b.map(s => s.toLowerCase()))
  return a.filter(item => bLower.has(item.toLowerCase()))
}

const listA = ['Apple', 'Banana', 'Cherry']
const listB = ['apple', 'BANANA', 'Date']
console.log(intersectionCaseInsensitive(listA, listB))  // ['Apple', 'Banana']

Store lowercase versions in bLower for comparison, return original elements from a.

Performance: Set vs Array#

Why use Set instead of arrays?

Operation Set Array Difference
Lookup has/includes O(1) O(n) Set is n× faster
Insert add/push O(1) O(1) Same
Deduplication Automatic Manual traversal Set is cleaner

The gap becomes massive at scale. Array intersection:

// Array version: O(n * m) complexity
function intersectionArray(a: string[], b: string[]): string[] {
  return a.filter(x => b.includes(x))  // includes is O(m)
}

// Set version: O(n) complexity
function intersectionSet(a: string[], b: string[]): string[] {
  const setB = new Set(b)  // O(m)
  return a.filter(x => setB.has(x))  // O(n) * O(1)
}

With 10,000 elements each: array version does 100 million comparisons, Set version does 20,000.

Edge Cases#

Empty Sets#

const emptySet = new Set()
union(emptySet, setA)      // Returns copy of setA
intersection(emptySet, setA)  // Returns empty Set
difference(emptySet, setA)    // Returns empty Set

Mathematically correct: union with empty set equals the original set, intersection with empty set is empty.

NaN Handling#

const set = new Set([NaN, NaN])
console.log(set.size)  // 1, Set treats NaN === NaN

This is actually expected behavior for set operations.

Object References#

const obj = { id: 1 }
const setA = new Set([obj])
const setB = new Set([{ id: 1 }])

console.log(intersection(setA, setB))  // Empty Set!

Set compares references, not values. For content comparison, serialize first or implement custom comparison.

Real-World Use Cases#

Comparing Config Files#

const oldConfig = ['debug', 'verbose', 'timeout']
const newConfig = ['debug', 'log-level', 'timeout']

const oldSet = new Set(oldConfig)
const newSet = new Set(newConfig)

console.log([...difference(newSet, oldSet)])  // ['log-level'] added
console.log([...difference(oldSet, newSet)])  // ['verbose'] removed

Permission Checking#

const requiredPermissions = new Set(['read', 'write', 'delete'])
const userPermissions = new Set(['read', 'write'])

const missing = difference(requiredPermissions, userPermissions)
console.log([...missing])  // ['delete'] - insufficient permissions

Tool Recommendation#

I built an online set operations tool: Set Operations

Features:

  • Union, intersection, difference, symmetric difference
  • Case sensitivity toggle
  • Auto-trim whitespace and empty lines
  • Real-time result count

Under 100 lines of core code, but it transforms mathematical elegance into practical utility.


Related tools: Text Deduplicate | JSON Diff