TypeScript Set

Introduced in ECMAScript 2015 (ES6), the Set object is a built-in collection designed to store unique values of any type. In the context of TypeScript, Set becomes even more powerful because it leverages generics to provide robust static type checking, ensuring that the elements you add to the collection conform to your defined type.

Unlike an Array, which allows duplicate entries and provides access via numerical indices, a Set is optimized for membership testing and ensuring data integrity through uniqueness. While arrays are better for ordered sequences where duplicates are allowed, Set is the superior choice when you need to perform frequent lookups, deletions, or simply want to maintain a collection of distinct items.

Working with Generics and Initialization

One of the primary advantages of using Set in TypeScript is the ability to define the type of elements it will contain using the Set<T> syntax.

Explicit Type Annotation

When you want to be explicit about the type of data the set will hold, you can declare it during instantiation:

const numbers = new Set<number>();
numbers.add(1);
numbers.add(2);
numbers.add(2); // This duplicate will be ignored

console.log(numbers.size); // Output: 2

You can also handle complex types by using union types:

const mixedCollection = new Set<string | number>();
mixedCollection.add("user_123");
mixedCollection.add(404);

Type Inference

TypeScript is often smart enough to infer the type of a Set if you initialize it with an array or another iterable:

const names = new Set(["Alice", "Bob", "Charlie"]); 
// TypeScript automatically infers this as Set<string>

Creating Sets from Iterables

You can initialize a Set using various iterable objects, such as arrays or even strings:

const fromArray = new Set<number>([1, 2, 3, 4]);
const fromString = new Set<string>("hello"); 
// Result: Set { 'h', 'e', 'l', 'o' }

Core API: Properties and Methods

The Set API is concise and focused on managing unique membership. Here are the most commonly used members:

  • add(value: T): this: Adds a new element to the set. Because it returns the set instance itself, you can use method chaining.
  • has(value: T): boolean: Returns true if the value exists in the set, and false otherwise. This is highly efficient.
  • delete(value: T): boolean: Removes the specified element. It returns true if the element was successfully found and removed, and false if the element did not exist.
  • clear(): void: Removes all elements from the set, resetting its size to zero.
  • size: number: A property that returns the total number of unique elements currently in the set.
const tags = new Set<string>();

// Using method chaining
tags.add("typescript").add("javascript").add("react");

console.log(tags.has("typescript")); // true
console.log(tags.size);             // 3

tags.delete("javascript");
console.log(tags.size);             // 2

tags.clear();
console.log(tags.size);             // 0

Iterating Over a Set

Since Set maintains the insertion order, iterating through its elements is predictable. You cannot access elements by index (e.g., set[0] will not work), so you must use iteration protocols.

Using for...of

The most idiomatic way to loop through a set is the for...of loop:

const colors = new Set(["red", "green", "blue"]);

for (const color of colors) {
  console.log(color);
}

Using forEach

Similar to arrays, Set provides a forEach method. Note a unique quirk: the callback function receives the value as both the first and second arguments (since Sets do not have keys).

colors.forEach((value, _unusedValue, set) => {
  console.log(value, `Total size: ${set.size}`);
});

Additionally, you can use values(), keys(), and entries(). In a Set, keys() and values() behave identically.

Practical Patterns: Conversion and Deduplication

Converting Between Set and Array

In many real-world scenarios, you may need to switch between a Set (for uniqueness) and an Array (for indexing or sorting).

const mySet = new Set([1, 2, 3]);

// Method 1: Using the spread operator (most common)
const arr1 = [...mySet];

// Method 2: Using Array.from()
const arr2 = Array.from(mySet);

The "De-duplication" Trick

The most popular use case for Set is removing duplicates from an existing array. This can be achieved in a single, elegant line:

const rawData = [1, 2, 2, 3, 3, 3, 4, 4, 5];
const uniqueData = [...new Set(rawData)];

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

Note: If your target environment is older than ES2015, ensure your tsconfig.json is configured with "target": "ES2015" or higher, or enable "downlevelIteration".

Implementing Set Mathematics

While JavaScript/TypeScript provides the foundation, it does not natively include methods for mathematical set operations like union, intersection, or difference. However, we can easily implement these using generics to maintain type safety.

/** Returns a new set containing all elements from both sets */
function union<T>(a: Set<T>, b: Set<T>): Set<T> {
  return new Set([...a, ...b]);
}

/** Returns a new set containing only elements present in both sets */
function intersection<T>(a: Set<T>, b: Set<T>): Set<T> {
  const result = new Set<T>();
  for (const item of a) {
    if (b.has(item)) {
      result.add(item);
    }
  }
  return result;
}

/** Returns a new set containing elements in 'a' that are NOT in 'b' */
function difference<T>(a: Set<T>, b: Set<T>): Set<T> {
  const result = new Set<T>();
  for (const item of a) {
    if (!b.has(item)) {
      result.add(item);
    }
  }
  return result;
}

// Usage Example
const setA = new Set([1, 2, 3]);
const setB = new Set([3, 4, 5]);

console.log([...union(setA, setB)]);        // [1, 2, 3, 4, 5]
console.log([...intersection(setA, setB)]); // [3]
console.log([...difference(setA, setB)]);   // [1, 2]

Advanced Considerations and Best Practices

Equality and Reference Types

Set uses the SameValueZero algorithm to determine equality. This has two important implications:

  1. NaN handling: Unlike some other comparison methods, NaN is considered equal to NaN in a Set, so you won't get duplicate NaN entries.
  2. Object References: For objects, arrays, or functions, Set compares references, not content. Two different object literals with the same properties are considered unique.
const set = new Set();
set.add({ id: 1 });
set.add({ id: 1 }); 

console.log(set.size); // 2 (because they are different object references)

Defensive Programming with ReadonlySet<T>

If you are passing a Set into a function that should only read the data without modifying it, use the ReadonlySet<T> type. This prevents accidental calls to .add() or .delete().

function processData(data: ReadonlySet<string>) {
  data.forEach(item => console.log(item));
  // data.add("new"); // Error: Property 'add' does not exist on type 'ReadonlySet<string>'
}

Memory Management: WeakSet

If you need to store objects as keys but want to avoid memory leaks, consider WeakSet. A WeakSet holds "weak" references to its elements, meaning if there are no other references to an object, it can be garbage collected even if it is still inside the WeakSet. Note that WeakSet is not iterable and can only store objects.

Configuration Tip

To ensure TypeScript recognizes the Set type, verify that your tsconfig.json includes ES2015 (or a later version) in the lib array:

{
  "compilerOptions": {
    "lib": ["ES2015", "DOM"]
  }
}