ts-data-forge
Version:
[](https://www.npmjs.com/package/ts-data-forge) [](https://www.npmjs.com/package/ts-data-forge) [ membership testing.
*
* ISetMapped allows you to use complex objects as set elements by providing transformation functions
* that convert between your custom element type `K` and a primitive `MapSetKeyType` `KM` that can
* be efficiently stored in JavaScript's native Set. This enables high-performance set operations
* on complex elements while maintaining type safety and immutability.
*
* **Key Features:**
* - **Custom Element Types**: Use any type as set elements by providing `toKey`/`fromKey` functions
* - **O(1) Performance**: Maintains O(1) average-case performance for membership testing and mutations
* - **Immutable**: All operations return new instances, preserving immutability
* - **Set Operations**: Full support for union, intersection, difference, subset/superset checks
* - **Type Safe**: Full TypeScript support with generic element types
*
* **Performance Characteristics:**
* - has/add/delete: O(1) average case (plus element transformation overhead)
* - Set operations (union, intersection, difference): O(n)
* - map/filter operations: O(n)
* - Iteration: O(n) (plus element transformation overhead)
*
* @template K The type of the custom elements in the set.
* @template KM The type of the mapped primitive keys (string, number, etc.).
*
* @example
* ```typescript
* // Example with complex object elements
* type User = { id: number; department: string; email: string };
*
* // Define transformation functions
* const userToKey = (user: User): string => `${user.department}:${user.id}`;
* const keyToUser = (key: string): User => {
* const [department, idStr] = key.split(':');
* // In practice, you might fetch from a cache or reconstruct more robustly
* return { id: Number(idStr), department, email: `user${idStr}@${department}.com` };
* };
*
* declare const activeUsers: ISetMapped<User, string>;
*
* // All operations work with the complex element type
* const user: User = { id: 123, department: "engineering", email: "alice@engineering.com" };
* const hasUser = activeUsers.has(user); // O(1)
* const withNewUser = activeUsers.add(user); // O(1) - returns new ISetMapped
* const withoutUser = activeUsers.delete(user); // O(1) - returns new ISetMapped
* ```
*/
type ISetMappedInterface<K, KM extends MapSetKeyType> = Readonly<{
/**
* Creates a new ISetMapped instance.
* @param iterable An iterable of elements.
* @param toKey A function that converts an element of type `K` to `KM`.
* @param fromKey A function that converts a mapped key of type `KM` back to `K`.
*/
new (iterable: Iterable<K>, toKey: (a: K) => KM, fromKey: (k: KM) => K): void;
/** The number of elements in the set. */
size: SizeType.Arr;
/** Checks if the set is empty. */
isEmpty: boolean;
/**
* Checks if an element exists in the set.
* @param key The element to check.
* @returns `true` if the element exists, `false` otherwise.
*/
has: (key: K) => boolean;
/**
* Checks if all elements in the set satisfy a predicate.
* @param predicate A function to test each element.
* @returns `true` if all elements satisfy the predicate, `false` otherwise.
*/
every: ((predicate: (key: K) => boolean) => boolean) &
/**
* Checks if all elements in the set satisfy a type predicate.
* Narrows the type of elements in the set if the predicate returns true for all elements.
* @template L The narrowed type of the elements.
* @param predicate A type predicate function.
* @returns `true` if all elements satisfy the predicate, `false` otherwise.
*/
(<L extends K>(predicate: (key: K) => key is L) => this is ISetMapped<L, KM>);
/**
* Checks if at least one element in the set satisfies a predicate.
* @param predicate A function to test each element.
* @returns `true` if at least one element satisfies the predicate, `false` otherwise.
*/
some: (predicate: (key: K) => boolean) => boolean;
/**
* Adds an element to the set.
* @param key The element to add.
* @returns A new ISetMapped instance with the element added.
*/
add: (key: K) => ISetMapped<K, KM>;
/**
* Deletes an element from the set.
* @param key The element to delete.
* @returns A new ISetMapped instance without the specified element.
*/
delete: (key: K) => ISetMapped<K, KM>;
/**
* Applies a series of mutations to the set.
* @param actions An array of mutation actions (add or delete).
* @returns A new ISetMapped instance with all mutations applied.
*/
withMutations: (actions: readonly Readonly<{
type: 'add';
key: K;
} | {
type: 'delete';
key: K;
}>[]) => ISetMapped<K, KM>;
/**
* Maps the elements of the set to new elements.
* Note: The element type `K` cannot be changed because `toKey` and `fromKey` would become unusable if the mapped type `KM` changes.
* @param mapFn A function that maps an element to a new element of the same type `K`.
* @returns A new ISetMapped instance with mapped elements.
*/
map: (mapFn: (key: K) => K) => ISetMapped<K, KM>;
/**
* Filters the elements of the set based on a predicate.
* @param predicate A function to test each element.
* @returns A new ISetMapped instance with elements that satisfy the predicate.
*/
filter: (predicate: (value: K) => boolean) => ISetMapped<K, KM>;
/**
* Filters the elements of the set by excluding elements for which the predicate returns true.
* @param predicate A function to test each element.
* @returns A new ISetMapped instance with elements for which the predicate returned `false`.
*/
filterNot: (predicate: (key: K) => boolean) => ISetMapped<K, KM>;
/**
* Executes a callback function for each element in the set.
* @param callbackfn A function to execute for each element.
*/
forEach: (callbackfn: (key: K) => void) => void;
/**
* Checks if this set is a subset of another set.
* @param set The other set.
* @returns `true` if this set is a subset of the other set, `false` otherwise.
*/
isSubsetOf: (set: ISetMapped<K, KM>) => boolean;
/**
* Checks if this set is a superset of another set.
* @param set The other set.
* @returns `true` if this set is a superset of the other set, `false` otherwise.
*/
isSupersetOf: (set: ISetMapped<K, KM>) => boolean;
/**
* Returns a new set with elements that are in this set but not in another set.
* @param set The other set.
* @returns A new ISetMapped instance representing the set difference.
*/
subtract: (set: ISetMapped<K, KM>) => ISetMapped<K, KM>;
/**
* Returns a new set with elements that are common to both this set and another set.
* @param set The other set.
* @returns A new ISetMapped instance representing the set intersection.
*/
intersect: (set: ISetMapped<K, KM>) => ISetMapped<K, KM>;
/**
* Returns a new set with all elements from both this set and another set.
* @param set The other set.
* @returns A new ISetMapped instance representing the set union.
*/
union: (set: ISetMapped<K, KM>) => ISetMapped<K, KM>;
/**
* Returns an iterator for the elements in the set (alias for values).
* @returns An iterable iterator of elements.
*/
keys: () => IterableIterator<K>;
/**
* Returns an iterator for the elements in the set.
* @returns An iterable iterator of elements.
*/
values: () => IterableIterator<K>;
/**
* Returns an iterator for the entries (element-element pairs) in the set.
* @returns An iterable iterator of entries.
*/
entries: () => IterableIterator<readonly [K, K]>;
/**
* Converts the elements of the set to an array.
* @returns A readonly array of elements.
*/
toArray: () => readonly K[];
/**
* Returns the underlying readonly JavaScript Set of mapped keys.
* @returns The raw ReadonlySet instance.
*/
toRawSet: () => ReadonlySet<KM>;
}>;
/**
* Represents an immutable set with custom element transformation and high-performance operations.
*
* ISetMapped is a specialized persistent data structure that enables using complex objects as set elements
* while maintaining the performance benefits of JavaScript's native Set. It achieves this by requiring
* bidirectional transformation functions that convert between your custom element type and a primitive type
* that can be efficiently stored and compared for uniqueness.
*
* **Key Features:**
* - **Complex Elements**: Use objects, arrays, or any custom type as set elements
* - **High Performance**: O(1) operations through efficient element transformation
* - **Immutable**: All mutation operations return new instances
* - **Type Safe**: Full TypeScript support with compile-time element type checking
* - **Bidirectional**: Maintains ability to reconstruct original elements from mapped keys
* - **Set Algebra**: Complete support for mathematical set operations
*
* **Use Cases:**
* - Sets of entities with complex identifiers
* - Deduplication of objects based on specific properties
* - Performance-critical sets with non-primitive elements
* - Mathematical set operations on complex data structures
*
* @template K The type of the custom elements in the set.
* @template KM The type of the mapped primitive keys (string, number, etc.).
*
* @example
* ```typescript
* // Example: User management with composite identity
* type User = { id: number; department: string; username: string; email: string };
*
* // Define bidirectional transformation functions
* const userToKey = (user: User): string => `${user.department}:${user.id}`;
* const keyToUser = (key: string): User => {
* const [department, idStr] = key.split(':');
* const id = Number(idStr);
* // In practice, this might fetch from a user service or cache
* return {
* id,
* department,
* username: `user${id}`,
* email: `user${id}@${department}.company.com`
* };
* };
*
* // Create a set with complex elements
* let activeUsers = ISetMapped.create<User, string>([], userToKey, keyToUser);
*
* // Use complex objects as elements naturally
* const alice: User = { id: 1, department: "engineering", username: "alice", email: "alice@engineering.company.com" };
* const bob: User = { id: 2, department: "marketing", username: "bob", email: "bob@marketing.company.com" };
* const charlie: User = { id: 3, department: "engineering", username: "charlie", email: "charlie@engineering.company.com" };
*
* activeUsers = activeUsers
* .add(alice)
* .add(bob)
* .add(charlie);
*
* // All operations work with the original element type
* console.log(activeUsers.has(alice)); // Output: true
* console.log(activeUsers.size); // Output: 3
*
* // Set operations preserve element types
* const engineeringUsers = ISetMapped.create<User, string>([alice, charlie], userToKey, keyToUser);
* const marketingUsers = ISetMapped.create<User, string>([bob], userToKey, keyToUser);
*
* const allUsers = ISetMapped.union(engineeringUsers, marketingUsers);
* const engineeringOnly = activeUsers.intersect(engineeringUsers);
*
* // Iteration preserves original element types
* for (const user of engineeringOnly) {
* console.log(`${user.username} works in ${user.department}`);
* }
* // Output:
* // alice works in engineering
* // charlie works in engineering
*
* // Functional transformations work seamlessly
* const updatedUsers = activeUsers.map(user => ({
* ...user,
* email: user.email.replace('.company.com', '.example.com')
* }));
* ```
*/
export type ISetMapped<K, KM extends MapSetKeyType> = Iterable<K> & Readonly<ISetMappedInterface<K, KM>>;
/**
* Provides utility functions for ISetMapped.
*/
export declare namespace ISetMapped {
/**
* Creates a new ISetMapped instance with custom element transformation functions.
*
* This factory function creates an immutable set that can use complex objects as elements
* by providing bidirectional transformation functions. The `toKey` function converts
* your custom element type to a primitive type that can be efficiently stored, while
* `fromKey` reconstructs the original element type for iteration and access.
*
* **Performance:** O(n) where n is the number of elements in the iterable.
*
* @template K The type of the custom elements.
* @template KM The type of the mapped primitive keys.
* @param iterable An iterable of elements using the custom element type.
* @param toKey A function that converts a custom element `K` to a primitive key `KM`.
* This function must be deterministic and produce unique values for unique elements.
* @param fromKey A function that converts a primitive key `KM` back to the custom element `K`.
* This should be the inverse of `toKey`.
* @returns A new ISetMapped instance containing all unique elements from the iterable.
*
* @example
* ```typescript
* // Example 1: Product catalog with SKU-based identity
* type Product = { sku: string; name: string; price: number; category: string };
*
* const productToKey = (product: Product): string => product.sku;
* const keyToProduct = (sku: string): Product => {
* // In practice, this might fetch from a product service or cache
* return {
* sku,
* name: `Product ${sku}`,
* price: 0,
* category: "unknown"
* };
* };
*
* const productSet = ISetMapped.create<Product, string>(
* [
* { sku: "LAPTOP-001", name: "Gaming Laptop", price: 1299, category: "electronics" },
* { sku: "MOUSE-002", name: "Wireless Mouse", price: 49, category: "accessories" },
* { sku: "LAPTOP-001", name: "Gaming Laptop", price: 1299, category: "electronics" } // Duplicate SKU
* ],
* productToKey,
* keyToProduct
* );
*
* console.log(productSet.size); // Output: 2 (duplicate removed)
* console.log(productSet.has({ sku: "LAPTOP-001", name: "Gaming Laptop", price: 1299, category: "electronics" })); // true
*
* // Example 2: Geographic locations with coordinate-based identity
* type Location = { name: string; lat: number; lng: number; type: string };
*
* const locationToKey = (loc: Location): string => `${loc.lat.toFixed(6)},${loc.lng.toFixed(6)}`;
* const keyToLocation = (key: string): Location => {
* const [latStr, lngStr] = key.split(',');
* return {
* name: "Unknown Location",
* lat: parseFloat(latStr),
* lng: parseFloat(lngStr),
* type: "point"
* };
* };
*
* const locationSet = ISetMapped.create<Location, string>(
* [
* { name: "Statue of Liberty", lat: 40.689247, lng: -74.044502, type: "monument" },
* { name: "Empire State Building", lat: 40.748817, lng: -73.985428, type: "building" }
* ],
* locationToKey,
* keyToLocation
* );
*
* // Example 3: User entities with multi-part identity
* type User = { id: number; tenant: string; email: string; active: boolean };
*
* const userToKey = (user: User): string => `${user.tenant}:${user.id}`;
* const keyToUser = (key: string): User => {
* const [tenant, idStr] = key.split(':');
* return {
* id: Number(idStr),
* tenant,
* email: `user${idStr}@${tenant}.com`,
* active: true
* };
* };
*
* const userSet = ISetMapped.create<User, string>(
* [],
* userToKey,
* keyToUser
* )
* .add({ id: 1, tenant: "acme", email: "alice@acme.com", active: true })
* .add({ id: 2, tenant: "acme", email: "bob@acme.com", active: false });
*
* console.log(userSet.size); // Output: 2
*
* // Example 4: Empty set with type specification
* const emptyProductSet = ISetMapped.create<Product, string>(
* [],
* productToKey,
* keyToProduct
* );
* console.log(emptyProductSet.isEmpty); // Output: true
* ```
*/
const create: <K, KM extends MapSetKeyType>(iterable: Iterable<K>, toKey: (a: K) => KM, fromKey: (k: KM) => K) => ISetMapped<K, KM>;
/**
* Checks if two ISetMapped instances are structurally equal.
*
* Two ISetMapped instances are considered equal if they have the same size and contain
* exactly the same elements. The comparison is performed on the underlying mapped keys,
* so the transformation functions themselves don't need to be identical. Elements are
* compared based on their mapped key representations.
*
* **Performance:** O(n) where n is the size of the smaller set.
*
* @template K The type of the custom elements.
* @template KM The type of the mapped primitive keys.
* @param a The first ISetMapped instance to compare.
* @param b The second ISetMapped instance to compare.
* @returns `true` if the sets contain exactly the same elements, `false` otherwise.
*
* @example
* ```typescript
* // Example with coordinate-based elements
* type Point = { x: number; y: number; label?: string };
* const pointToKey = (p: Point): string => `${p.x},${p.y}`;
* const keyToPoint = (s: string): Point => {
* const [x, y] = s.split(',').map(Number);
* return { x, y };
* };
*
* const set1 = ISetMapped.create<Point, string>(
* [{ x: 1, y: 2, label: "A" }, { x: 3, y: 4, label: "B" }],
* pointToKey,
* keyToPoint
* );
*
* const set2 = ISetMapped.create<Point, string>(
* [{ x: 3, y: 4, label: "Different" }, { x: 1, y: 2, label: "Labels" }], // Order doesn't matter
* pointToKey,
* keyToPoint
* );
*
* const set3 = ISetMapped.create<Point, string>(
* [{ x: 1, y: 2 }, { x: 5, y: 6 }], // Different point
* pointToKey,
* keyToPoint
* );
*
* console.log(ISetMapped.equal(set1, set2)); // true (same coordinates, labels don't affect equality)
* console.log(ISetMapped.equal(set1, set3)); // false (different coordinates)
*
* // Example with user entities
* type User = { id: number; department: string; name: string };
* const userToKey = (u: User): string => `${u.department}:${u.id}`;
* const keyToUser = (k: string): User => {
* const [department, idStr] = k.split(':');
* return { id: Number(idStr), department, name: "" };
* };
*
* const users1 = ISetMapped.create<User, string>(
* [
* { id: 1, department: "eng", name: "Alice" },
* { id: 2, department: "sales", name: "Bob" }
* ],
* userToKey,
* keyToUser
* );
*
* const users2 = ISetMapped.create<User, string>(
* [
* { id: 2, department: "sales", name: "Robert" }, // Different name, same identity
* { id: 1, department: "eng", name: "Alicia" } // Different name, same identity
* ],
* userToKey,
* keyToUser
* );
*
* console.log(ISetMapped.equal(users1, users2)); // true (same department:id combinations)
*
* // Empty sets
* const empty1 = ISetMapped.create<Point, string>([], pointToKey, keyToPoint);
* const empty2 = ISetMapped.create<Point, string>([], pointToKey, keyToPoint);
* console.log(ISetMapped.equal(empty1, empty2)); // true
*
* // Sets with different transformation functions but same logical content
* const alternativePointToKey = (p: Point): string => `(${p.x},${p.y})`; // Different format
* const alternativeKeyToPoint = (s: string): Point => {
* const match = s.match(/\((\d+),(\d+)\)/)!;
* return { x: Number(match[1]), y: Number(match[2]) };
* };
*
* const set4 = ISetMapped.create<Point, string>(
* [{ x: 1, y: 2 }, { x: 3, y: 4 }],
* alternativePointToKey,
* alternativeKeyToPoint
* );
*
* // This would be false because the underlying mapped keys are different
* console.log(ISetMapped.equal(set1, set4)); // false
* ```
*/
const equal: <K, KM extends MapSetKeyType>(a: ISetMapped<K, KM>, b: ISetMapped<K, KM>) => boolean;
/**
* Computes the difference between two ISetMapped instances.
* @template K The type of the elements.
* @template KM The type of the mapped keys.
* @param oldSet The original set.
* @param newSet The new set.
* @returns An object containing sets of added and deleted elements.
* @example
* ```typescript
* type Tag = { name: string };
* const tagToKey = (t: Tag): string => t.name;
* const keyToTag = (name: string): Tag => ({ name });
*
* const oldTags = ISetMapped.create<Tag, string>(
* [{ name: "typescript" }, { name: "javascript" }],
* tagToKey,
* keyToTag
* );
* const newTags = ISetMapped.create<Tag, string>(
* [{ name: "javascript" }, { name: "react" }, { name: "nextjs" }],
* tagToKey,
* keyToTag
* );
*
* const diffResult = ISetMapped.diff(oldTags, newTags);
*
* console.log("Deleted tags:", diffResult.deleted.toArray().map(t => t.name));
* // Output: Deleted tags: ["typescript"]
*
* console.log("Added tags:", diffResult.added.toArray().map(t => t.name));
* // Output: Added tags: ["react", "nextjs"]
* ```
*/
const diff: <K, KM extends MapSetKeyType>(oldSet: ISetMapped<K, KM>, newSet: ISetMapped<K, KM>) => ReadonlyRecord<"added" | "deleted", ISetMapped<K, KM>>;
/**
* Computes the intersection of two ISetMapped instances.
* @template K The type of the elements.
* @template KM The type of the mapped keys.
* @param a The first set.
* @param b The second set.
* @returns A new ISetMapped instance representing the intersection.
* @example
* ```typescript
* type Permission = { id: string };
* const permToKey = (p: Permission): string => p.id;
* const keyToPerm = (id: string): Permission => ({ id });
*
* const userPermissions = ISetMapped.create<Permission, string>(
* [{ id: "read" }, { id: "write" }, { id: "delete" }],
* permToKey,
* keyToPerm
* );
* const rolePermissions = ISetMapped.create<Permission, string>(
* [{ id: "read" }, { id: "comment" }, { id: "write" }],
* permToKey,
* keyToPerm
* );
*
* const commonPermissions = ISetMapped.intersection(userPermissions, rolePermissions);
* console.log(commonPermissions.toArray().map(p => p.id)); // Output: ["read", "write"]
* ```
*/
const intersection: <K, KM extends MapSetKeyType>(a: ISetMapped<K, KM>, b: ISetMapped<K, KM>) => ISetMapped<K, KM>;
/**
* Computes the union of two ISetMapped instances.
* @template K The type of the elements.
* @template KM The type of the mapped keys.
* @param a The first set.
* @param b The second set.
* @returns A new ISetMapped instance representing the union.
* @example
* ```typescript
* type FeatureFlag = { flagName: string };
* const flagToKey = (f: FeatureFlag): string => f.flagName;
* const keyToFlag = (name: string): FeatureFlag => ({ flagName: name });
*
* const setA = ISetMapped.create<FeatureFlag, string>(
* [{ flagName: "newUI" }, { flagName: "betaFeature" }],
* flagToKey,
* keyToFlag
* );
* const setB = ISetMapped.create<FeatureFlag, string>(
* [{ flagName: "betaFeature" }, { flagName: "darkMode" }],
* flagToKey,
* keyToFlag
* );
*
* const combinedFlags = ISetMapped.union(setA, setB);
* // The order might vary as sets are unordered internally.
* console.log(combinedFlags.toArray().map(f => f.flagName).toSorted());
* // Output: ["betaFeature", "darkMode", "newUI"]
* ```
*/
const union: <K, KM extends MapSetKeyType>(a: ISetMapped<K, KM>, b: ISetMapped<K, KM>) => ISetMapped<K, KM>;
}
export {};
//# sourceMappingURL=iset-mapped.d.mts.map