Uniq — Unique Collections
Uniqueness is a common requirement in data modeling. We often need to track a collection of items where duplicates have no meaning—such as a list of active user roles, a set of enabled feature flags, or a collection of tags assigned to an article.
In standard JavaScript, the native Set object provides this behavior. However, the native Set API is built entirely around mutation. Adding or removing an item modifies the collection in place. This makes it difficult to reason about the state of our data over time, as any function that receives a set can alter its contents under our feet.
Uniq treats uniqueness as an immutable value. Instead of modifying a set in place, operations on Uniq return a new, independent set, leaving the original collection completely untouched.
The problem with mutation in sets
Section titled “The problem with mutation in sets”Consider a scenario where we manage a user’s permissions. We might want to temporarily grant a permission for a specific operation, or calculate a new set of permissions without altering the user’s permanent profile.
With the native Set API, we are forced to manually copy the set to avoid accidental side effects:
If we forget to make this copy, we introduce a bug where the user permanently gains the "admin" permission. The native Set API conflates the identity of the collection with its current state.
The shift to immutable uniqueness
Section titled “The shift to immutable uniqueness”Uniq separates identity from state. Every modification returns a new set representing the new state, while the original remains unchanged. Furthermore, Uniq is designed to be highly efficient: if an operation would result in no change (such as inserting a value that is already present, or removing a value that is absent), Uniq returns the original set reference, avoiding unnecessary memory allocations.
flowchart TD
A["Uniq (Original)"] --> B["insert('admin')"]
B --> C["New Uniq (With admin)"]
A --> D["Original Uniq (Unchanged)"]
Creating collections
Section titled “Creating collections”We can lift raw arrays or individual elements into an immutable Uniq collection using constructors:
Checking membership
Section titled “Checking membership”To inspect the contents of a collection, we use Uniq.has and Uniq.isSubsetOf. Because these functions are curried and place the collection as the last argument, they fit cleanly into composition pipelines:
Adding and removing items
Section titled “Adding and removing items”Adding or removing items returns a new Uniq collection. If the operation does not change the membership of the set, the original reference is preserved:
Transforming and filtering collections
Section titled “Transforming and filtering collections”We can transform the elements of a collection or filter them using pure functions. If a transformation produces duplicate values, Uniq automatically merges them to maintain uniqueness:
Classic set operations
Section titled “Classic set operations”Uniq provides pure, immutable implementations of classic set algebra operations: union, intersection, and difference. These are useful when reconciling permissions, merging configuration profiles, or calculating differentials between two states.
Folding and converting to standard types
Section titled “Folding and converting to standard types”When it is time to leave the immutable context—either to serialize data for an API or to interface with a library that requires standard arrays—we can fold or convert our collection:
When to use Uniq vs Standard Set
Section titled “When to use Uniq vs Standard Set”Use Uniq when
Section titled “Use Uniq when”- You are writing functional pipelines using
pipeand need data-last, curried operations. - You want to guarantee immutability across your application layers, ensuring that functions cannot accidentally modify collections passed to them.
- You require fast, pure set operations like
union,intersection, ordifferencewithout managing temporary variables or copying sets. - You benefit from reference-equality optimizations (where unchanged operations return the exact same object reference).
Use Standard Set when
Section titled “Use Standard Set when”- You are writing highly critical, mutable, performance-sensitive loops where the overhead of allocating new object references must be avoided.
- You are interfacing with third-party libraries that expect a mutable
Setand actively mutate it.