What a hash set is
A hash set is a collection of unique values. Adding a value that is already there changes nothing, and there is no way to store the same value twice. In return, the three operations you use most are fast: adding a value, checking whether a value is present, and removing a value each take O(1) time on average.
In TypeScript the hash set is the built-in Set, the same object JavaScript has. Reach for it whenever the question is "have I seen this before?" or "which distinct values are in here?". Scanning an array to answer those takes O(n) per question. A set answers in constant time.
Creating a set
An empty set is created with new Set(). Because an empty set gives TypeScript nothing to infer from, say what it holds with a type argument:
const seen = new Set<number>();
seen.add(1);
seen.add("a"); // compiler error: a string is not a number
The type argument and the annotation are checked only by the compiler. When the code is compiled to JavaScript they disappear, so the running program has no idea that seen was meant to hold numbers.
A set can also be built from any iterable. Passing an array keeps one copy of each value:
const elements = [1, '2', 'apple', 1];
const mySet = new Set(elements);
console.log(mySet);
Set(3) { 1, '2', 'apple' }
TypeScript infers Set<string | number> here. Notice that 1 and '2' are different values: a set never converts between types. Building a set from an array of n elements takes O(n) time.
The operations
add(value)inserts the value and returns the set itself, so calls can be chained. Adding an existing value does nothing and raises no error.has(value)returnstrueorfalse.delete(value)removes the value and returnstrueif it was there,falseif not. Deleting a missing value is safe.sizeis a property, not a method. WritemySet.size, with no parentheses.for (const v of mySet)visits every value in the order it was first inserted.
const nums = new Set<number>([3, 1, 2]);
console.log(nums.add(2) === nums); // true: add returns the set
console.log(nums.delete(2)); // true
console.log(nums.delete(99)); // false
console.log(nums.has(1), nums.size);
true
true
false
true 2
Iterating a set takes O(n) time and the set itself uses O(n) space.
Turning a set back into an array
A set has no indexing and no map or filter. To get an array, spread it or use Array.from:
const unique = [...new Set([3, 1, 3, 2, 1])];
console.log(unique); // [ 3, 1, 2 ]
What counts as "the same" value
This is where sets surprise people:
- Objects and arrays compare by reference.
new Set([[1], [1]]).sizeis2, because the two arrays are different objects even though their contents match. To store pairs, turn them into a string key such as"1,2"first. - Primitives compare by value. Two equal numbers or two equal strings are one entry.
NaNequals itself in a set.new Set([NaN, NaN]).sizeis1, even thoughNaN === NaNisfalse.- The type does not matter at run time. A
Set<number>built from data that really holds strings, for example parsed input cast withas, will happily hold both1and"1".
The exercise, step by step
- Start with an array that contains a duplicate.
fruitsArrayis annotated asstring[]and holds"apples","bananas"and"apples". - Build the set from it.
fruitsis aSet<string>created withnew Set(fruitsArray). The second"apples"is dropped, so the set holds two values. - Add a new value.
fruits.add("oranges")grows the set to three values. Adding"apples"again would have changed nothing. - Check for a value.
fruits.has("apples")is the condition of anif. It istruehere, so the program printsWe have apples. - Remove a value.
fruits.delete("bananas")takes it out. - Loop over what is left. A
for...ofloop withlet fruitprints each remaining value.
let fruitsArray: string[] = ["apples", "bananas", "apples"];
let fruits: Set<string> = new Set(fruitsArray);
fruits.add("oranges");
if (fruits.has("apples")) {
console.log("We have apples");
}
fruits.delete("bananas");
for (let fruit of fruits) {
console.log(fruit);
}
The output is:
We have apples
apples
oranges
The loop prints apples before oranges because a set remembers insertion order. The duplicate and the deleted "bananas" are both gone.
Common mistakes
- Writing
mySet.size().sizeis a number, so calling it is an error. - Indexing a set.
mySet[0]is not valid. Spread the set into an array first if you need positions. - Expecting
addto report whether the value was new. It returns the set. Checkhasfirst, or comparesizebefore and after. - Using a set for objects you want compared by content. Two equal-looking objects are two entries.
- Using
Setfor counts. A set remembers that a value exists, not how many times it appeared. For counts, use aMapor an object.
Key takeaways
Set<T>stores each value once;add,hasanddeletetakeO(1)time on average.- Build one from an array to remove duplicates, and spread it to get an array back.
- Primitives compare by value and objects by reference.
- Iteration follows insertion order, and a set of
nvalues usesO(n)space. - The type argument is a compile-time check only.