Set
Sets and Reference Equality in TypeScript
The Core Concept
Sets in TypeScript (and JavaScript) use reference equality for objects, not value equality. This means Sets compare objects by their memory reference, not by their content.
What This Means in Practice
const set = new Set();
const obj1 = { id: 1, name: 'Alice' };
const obj2 = { id: 1, name: 'Alice' }; // Same values, different object
set.add(obj1);
set.add(obj2);
console.log(set.size); // 2 (not 1!)
Even though obj1
and obj2
have identical properties, they're treated as completely different by the Set because they're different objects in memory.
Key Behaviors
✅ Same reference = Same object
const obj = { id: 1 };
set.add(obj);
set.add(obj); // Adding same reference again
console.log(set.size); // Still 1 - duplicates prevented
❌ Same values ≠ Same object
set.add({ id: 1 });
set.has({ id: 1 }); // false - different object, even with same values
🔄 Mutations affect the stored object
const user = { name: 'Bob' };
set.add(user);
user.name = 'Robert'; // Mutating the object
// Set still contains the reference, now with updated value
When This Matters
- Deleting objects: You need the exact reference to delete an object from a Set
- Checking existence:
set.has()
only returns true for the exact same reference - Preventing duplicates: Only works for the same reference, not equivalent objects
Common Workarounds
If you need value-based equality:
- Use primitive values (strings, numbers) instead of objects
- Serialize objects:
set.add(JSON.stringify(obj))
- Use a Map with a unique key:
map.set(obj.id, obj)
- Create a custom wrapper class with value-based equality methods
The Bottom Line
Think of a Set as storing pointers to objects, not the objects' contents. Two houses might be identical in every way, but they're still two different houses at different addresses.
Examples
GT-Sandbox-Snapshot
Code
import { describe, it, expect, beforeEach } from 'vitest';
describe('Objects in Sets - TypeScript', () => {
describe('Object Reference Equality', () => {
it('should add and delete objects by reference', () => {
const set = new Set<{ id: number; name: string }>();
const obj1 = {id: 1, name: 'Alice'};
const obj2 = {id: 2, name: 'Bob'};
// Add objects
set.add(obj1);
set.add(obj2);
expect(set.size).toBe(2);
expect(set.has(obj1)).toBe(true);
expect(set.has(obj2)).toBe(true);
// Delete by reference works
set.delete(obj1);
expect(set.size).toBe(1);
expect(set.has(obj1)).toBe(false);
expect(set.has(obj2)).toBe(true);
});
it('should NOT recognize equivalent objects as the same', () => {
const set = new Set<{ id: number; name: string }>();
const obj1 = {id: 1, name: 'Alice'};
const obj2 = {id: 1, name: 'Alice'}; // Same values, different reference
set.add(obj1);
set.add(obj2);
// Both are added because they're different references
expect(set.size).toBe(2);
expect(set.has(obj1)).toBe(true);
expect(set.has(obj2)).toBe(true);
// Creating a new object with same values doesn't match
const obj3 = {id: 1, name: 'Alice'};
expect(set.has(obj3)).toBe(false);
});
it('should prevent duplicate references', () => {
const set = new Set<{ value: number }>();
const obj = {value: 42};
set.add(obj);
set.add(obj); // Adding same reference again
set.add(obj); // And again
// Only one entry because it's the same reference
expect(set.size).toBe(1);
});
});
describe('Mutating Objects in Sets', () => {
it('should reflect mutations to objects in the set', () => {
interface User {
id: number;
name: string;
}
const set = new Set<User>();
const user: User = {id: 1, name: 'Alice'};
set.add(user);
expect([...set][0].name).toBe('Alice');
// Mutate the object
user.name = 'Alice Smith';
// Set still has the same reference, now with updated value
expect(set.has(user)).toBe(true);
expect([...set][0].name).toBe('Alice Smith');
expect(set.size).toBe(1);
});
});
describe('Working with Complex Objects', () => {
class Person {
constructor(public id: number, public name: string) {
}
}
it('should handle class instances by reference', () => {
const set = new Set<Person>();
const person1 = new Person(1, 'Alice');
const person2 = new Person(1, 'Alice'); // Same values, different instance
set.add(person1);
set.add(person2);
// Both are in the set (different instances)
expect(set.size).toBe(2);
// Only the exact instance can be found/deleted
expect(set.has(person1)).toBe(true);
expect(set.has(new Person(1, 'Alice'))).toBe(false);
set.delete(person1);
expect(set.size).toBe(1);
expect(set.has(person2)).toBe(true);
});
});
describe('Practical Workarounds', () => {
it('should use Map for value-based lookups', () => {
// Using Map with a string key for value-based equality
const map = new Map<string, { id: number; name: string }>();
const obj1 = {id: 1, name: 'Alice'};
const obj2 = {id: 1, name: 'Alice'};
// Use a string key based on object values
const getKey = (obj: { id: number }) => `id:${obj.id}`;
map.set(getKey(obj1), obj1);
// Can find by value using the key
expect(map.has(getKey(obj2))).toBe(true);
expect(map.get(getKey(obj2))).toEqual(obj1);
});
it('should use JSON.stringify for simple value comparison', () => {
// Set of serialized objects for value-based uniqueness
const set = new Set<string>();
const obj1 = {id: 1, name: 'Alice'};
const obj2 = {id: 1, name: 'Alice'};
const obj3 = {id: 2, name: 'Bob'};
set.add(JSON.stringify(obj1));
set.add(JSON.stringify(obj2)); // Won't be added (same JSON)
set.add(JSON.stringify(obj3));
expect(set.size).toBe(2);
expect(set.has(JSON.stringify({id: 1, name: 'Alice'}))).toBe(true);
});
});
describe('Array and Nested Object Behavior', () => {
it('should handle arrays by reference', () => {
const set = new Set<number[]>();
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3]; // Same values, different array
set.add(arr1);
set.add(arr2);
expect(set.size).toBe(2); // Both arrays are in the set
expect(set.has(arr1)).toBe(true);
expect(set.has(arr2)).toBe(true);
expect(set.has([1, 2, 3])).toBe(false); // New array not recognized
});
it('should handle nested objects by reference', () => {
const set = new Set<{ user: { id: number } }>();
const nested1 = {user: {id: 1}};
const nested2 = {user: {id: 1}};
set.add(nested1);
set.add(nested2);
expect(set.size).toBe(2); // Different references
// Modifying nested object
nested1.user.id = 999;
expect(set.has(nested1)).toBe(true);
expect([...set][0].user.id).toBe(999);
});
});
});
Command to reproduce:
gt.sandbox.checkout.commit 22fa526ca9b9c3855fc3 \
&& cd "${GT_SANDBOX_REPO}" \
&& cmd.run.announce "./run.sh"
Recorded output of command:
up to date, audited 179 packages in 2s
33 packages are looking for funding
run `npm fund` for details
2 moderate severity vulnerabilities
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
> glassthought-sandbox@1.0.0 test
> vitest run
RUN v0.34.6 /home/nickolaykondratyev/git_repos/glassthought-sandbox
✓ src/main.test.ts (9 tests) 3ms
Test Files 1 passed (1)
Tests 9 passed (9)
Start at 21:39:46
Duration 497ms (transform 50ms, setup 0ms, collect 17ms, tests 3ms, environment 0ms, prepare 126ms)