Skip to content

Uniq — Unique Collections

JavaScript’s native Set API is mutable: .add() and .delete() modify collections in place, requiring defensive copying (new Set(existing)) to prevent accidental mutations across function boundaries.

Uniq provides curried, immutable operations over sets. Every operation returns a fresh collection, preserving the original set.

Managing permission sets without immutable operations requires manual copying:

const userPermissions = new Set(["read", "write"]);

// To calculate permissions with a temporary admin access without mutating the original:
const temporaryPermissions = new Set(userPermissions);
temporaryPermissions.add("admin");

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.

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)"]

We can lift raw arrays or individual elements into an immutable Uniq collection using constructors:

import { Uniq } from "@nlozgachev/pipelined/data";

// Create a collection from an array, automatically discarding duplicates
const tags = Uniq.from.Array(["typescript", "functional", "typescript", "pipe"]);
// Contains: "typescript", "functional", "pipe"

// Create a collection with a single starting value
const adminRole = Uniq.singleton("admin");

// Create an empty collection
const emptyFlags = Uniq.empty<string>();

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:

import { pipe } from "@nlozgachev/pipelined/composition";
import { Uniq } from "@nlozgachev/pipelined/data";

const permissions = Uniq.from.Array(["read", "write"]);

// Test for direct membership
const canWrite = pipe(permissions, Uniq.has("write")); // true
const canDelete = pipe(permissions, Uniq.has("delete")); // false

// Test if one collection is completely contained within another
const required = Uniq.from.Array(["read", "write"]);
const hasRequired = pipe(required, Uniq.isSubsetOf(permissions)); // true

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:

const roles = Uniq.from.Array(["editor", "viewer"]);

// Inserting a new item returns a new set
const updatedRoles = pipe(roles, Uniq.insert("admin"));
// Contains: "editor", "viewer", "admin"

// Inserting an existing item returns the original set reference
const identicalRoles = pipe(roles, Uniq.insert("editor"));
Object.is(roles, identicalRoles); // true

// Removing an item works similarly
const reducedRoles = pipe(roles, Uniq.remove("viewer"));
// Contains: "editor"

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:

const tags = Uniq.from.Array(["TypeScript", "typescript", "CSS"]);

// Normalise all tags to lowercase
const normalised = pipe(
  tags,
  Uniq.map(tag => tag.toLowerCase())
);
// Contains: "typescript", "css"

// Keep only tags that match a condition
const shortTags = pipe(
  tags,
  Uniq.filter(tag => tag.length <= 3)
);
// Contains: "CSS"

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.

const backendRoles = Uniq.from.Array(["alice", "bob", "carol"]);
const frontendRoles = Uniq.from.Array(["bob", "carol", "dave"]);

// Combine both collections (All engineers across both teams)
const allEngineers = pipe(backendRoles, Uniq.union(frontendRoles));
// Contains: "alice", "bob", "carol", "dave"

// Find common members (Engineers who are on both teams)
const fullStack = pipe(backendRoles, Uniq.intersection(frontendRoles));
// Contains: "bob", "carol"

// Find members unique to the first collection (Backend engineers not on frontend)
const backendOnly = pipe(backendRoles, Uniq.difference(frontendRoles));
// Contains: "alice"

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:

const activeFlags = Uniq.from.Array(["logging", "analytics"]);

// Convert back to a standard JavaScript array
const arrayFlags = Uniq.to.Array(activeFlags); // ["logging", "analytics"]

// Fold the collection into a single value
const totalLength = pipe(
  activeFlags,
  Uniq.reduce(0, (accumulator, flag: string) => accumulator + flag.length)
); // 16
  • Managing unique collections in immutable state trees: In React, Vue, or Zustand state models, mutating native Set objects in place bypasses change detection and causes UI rendering bugs. Uniq enforces structural immutability, returning fresh references on updates while reusing existing references when operations produce no changes.
  • Permission algebra and role reconciliation: In role-based access control (RBAC), applications frequently need to compute missing permissions (Uniq.difference), combine group privileges (Uniq.union), or find overlapping scopes (Uniq.intersection). Uniq provides pure, point-free set algebra operations.
  • Entitlement and prerequisite checks (Uniq.subset): Verifying that a user’s permission set covers all required scopes for an API endpoint or that a cart’s tags meet promotional prerequisites in a single declarative comparison.
  • Deduplicating tag lists and selected filters: In search filter components and tag editors, adding, toggling, or removing selected items inside pipe pipelines without managing temporary arrays or mutable set instances.
  • Point-free set mapping and aggregation: Transforming or filtering unique collections (such as mapping user IDs to names while maintaining uniqueness) with Uniq.map, Uniq.filter, and Uniq.reduce.