Set Operations in JavaScript: From Math Symbols to Code
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