Associum provides multi-key maps built on top of Map with full TypeScript support.
A vanilla map just compares its keys by reference, and does not reason about the structure of the given key. Associum's multi-key-maps solve this problem without falling back to serializing the whole key object, which would be memory-inefficient and make queries hard.
If you ever wanted to associate ["plant", "edible", "leafy"] => "spinach" or {concept: "mobility", subconcept: "car", data: "manufacturers"} => ["Mercedes", "BMW", "Toyota", "GM", "Ford"] this library provides the tools to make these associations and query them.
- Multiple Indexing Options: Support for unordered arrays, ordered arrays, or records (string-value pairs) as composite keys.
- Queryability: Optional mixin enabling partial key queries for flexible lookups (adds ~1KB to bundle size).
- Map Compatibility: Extends the built-in
Map, preserving standard methods while enhancing key interpretation. - Memory Efficient: Uses unique keylet IDs for subkeys with per-instance management and reference-counted garbage collection, achieving memory usage comparable to a vanilla
Map. - Lightweight: Tree-shakeable; base unordered variant ~200 bytes minified + gzipped (varies with build).
- Type Safe: Comprehensive TypeScript generics for keys and values.
- Universal: Compatible with browsers, Node.js, Deno, and other JS environments.
- Instance Isolated: Self-contained keylet registry per map prevents interference across instances.
- Key Safety: All indexing rejects
undefinedkeys to ensure valid composites.
Install via npm:
npm install associumImport and use like a standard Map, selecting the appropriate type:
import { UnorderedMultiKeyMap } from 'associum';
const multiKeyMap = new UnorderedMultiKeyMap<string[], any>();
multiKeyMap.set(["a", "c", "b"], 123);
if (multiKeyMap.has(["a", "b", "c"]))
console.log(multiKeyMap.get(["b", "a", "c"])); // 123Associum supports three key types:
Unordered keys are arrays where the order of elements doesn't matter. When you set a value with ["A", "B"], you can retrieve it with either ["A", "B"] or ["B", "A"]. This is useful when the combination of values is important, but not their sequence.
const map = new UnorderedMultiKeyMap<string[], number>();
map.set(["plant", "edible", "leafy"], 1);
console.log(map.get(["leafy", "plant", "edible"])); // 1Ordered keys are arrays where the order of elements matters. When you set a value with ["A", "B"], you can only retrieve it with the exact same order ["A", "B"]. This is useful when sequence is important.
const map = new OrderedMultiKeyMap<string[], number>();
map.set(["concept", "mobility", "car"], 1);
console.log(map.get(["concept", "mobility", "car"])); // 1
console.log(map.get(["mobility", "concept", "car"])); // undefinedStructured keys are objects where the first level of properties acts as the key. This provides a more semantic way to define keys with named properties.
const map = new StructuredMultiKeyMap<{concept: string, subconcept: string, data: string}, string[]>();
map.set({concept: "mobility", subconcept: "car", data: "manufacturers"}, ["Mercedes", "BMW", "Toyota"]);
console.log(map.get({concept: "mobility", subconcept: "car", data: "manufacturers"})); // ["Mercedes", "BMW", "Toyota"]Note: undefined values are not allowed for setting any type of key; attempts to set such keys throw an error.
Associum provides optional queryability that allows you to retrieve entries based on partial key matches. This feature is available through queryable variants of each map type.
With queryable unordered maps, you can find entries that contain specific key values regardless of their position in the array.
const map = new QueryableUnorderedMultiKeyMap<string[], number>();
map.set(["plant", "edible", "leafy"], 1);
map.set(["plant", "edible", "root"], 2);
map.set(["animal", "mammal", "domestic"], 3);
// Find entries with "plant" and "edible"
const results = map.queryIndexedWith(["plant", "edible"]);
// [{ key: ["plant", "edible", "leafy"], value: 1 }, { key: ["plant", "edible", "root"], value: 2 }]Note: Unordered maps do not support positional query; use queryIndexedWith for subset matches.
Match specific positions with wildcards (undefined). Use query.
const map = new QueryableOrderedMultiKeyMap<string[], number>();
map.set(["concept", "mobility", "car"], 1);
map.set(["concept", "housing", "apartment"], 2);
map.set(["concept", "mobility", "bike"], 3);
// Match position 0="concept", 1="mobility"
const results = map.query(["concept", "mobility", undefined]);
// [{ key: ["concept", "mobility", "car"], value: 1 }, { key: ["concept", "mobility", "bike"], value: 3 }]Supports prefix/suffix matching via wildcards.
With queryable structured maps, you can find entries that match specific property values, omitting properties you don't care about.
const map = new QueryableStructuredMultiKeyMap<{user: string, role: string, department: string}, number>();
map.set({user: "u1", role: "admin", department: "IT"}, 1);
map.set({user: "u1", role: "editor", department: "Marketing"}, 2);
map.set({user: "u2", role: "admin", department: "IT"}, 3);
// Find all entries where user is "u1"
const results = map.query({user: "u1"});
// [{ key: {user: "u1", role: "admin", department: "IT"}, value: 1 }, { key: {user: "u1", role: "editor", department: "Marketing"}, value: 2 }]Field order is fixed per instance based on first usage.
import { UnorderedMultiKeyMap } from 'associum';
// Create a map where key order doesn't matter
const plantMap = new UnorderedMultiKeyMap<string[], string>();
// Set values
plantMap.set(["plant", "edible", "leafy"], "spinach");
plantMap.set(["plant", "edible", "root"], "carrot");
plantMap.set(["plant", "inedible", "decorative"], "poison ivy");
// Get values - order doesn't matter
console.log(plantMap.get(["edible", "plant", "leafy"])); // "spinach"
console.log(plantMap.get(["plant", "root", "edible"])); // "carrot"
// Check existence
console.log(plantMap.has(["decorative", "plant", "inedible"])); // true
// Delete
plantMap.delete(["plant", "inedible", "decorative"]);
console.log(plantMap.has(["plant", "inedible", "decorative"])); // falseimport { OrderedMultiKeyMap } from 'associum';
// Create a map where key order matters
const conceptMap = new OrderedMultiKeyMap<string[], string[]>();
// Set values
conceptMap.set(["concept", "mobility", "car", "manufacturers"], ["Mercedes", "BMW", "Toyota", "GM", "Ford"]);
conceptMap.set(["concept", "mobility", "bike", "manufacturers"], ["Giant", "Trek", "Specialized"]);
conceptMap.set(["concept", "housing", "apartment", "types"], ["studio", "1BR", "2BR", "penthouse"]);
// Get values - order matters
console.log(conceptMap.get(["concept", "mobility", "car", "manufacturers"])); // ["Mercedes", "BMW", "Toyota", "GM", "Ford"]
console.log(conceptMap.get(["mobility", "concept", "car", "manufacturers"])); // undefined
// Check existence
console.log(conceptMap.has(["concept", "housing", "apartment", "types"])); // trueimport { StructuredMultiKeyMap } from 'associum';
// Create a map with object keys
const userMap = new StructuredMultiKeyMap<{user: string, role: string, department: string}, number>();
// Set values
userMap.set({user: "u1", role: "admin", department: "IT"}, 1);
userMap.set({user: "u1", role: "editor", department: "Marketing"}, 2);
userMap.set({user: "u2", role: "admin", department: "IT"}, 3);
// Get values
console.log(userMap.get({user: "u1", role: "admin", department: "IT"})); // 1
console.log(userMap.get({user: "u1", role: "admin"})); // undefined (requires full key)
// Check existence
console.log(userMap.has({user: "u2", role: "admin", department: "IT"})); // trueimport { QueryableUnorderedMultiKeyMap } from 'associum';
const tagMap = new QueryableUnorderedMultiKeyMap<string[], string>();
tagMap.set(["frontend", "react", "javascript"], "React");
tagMap.set(["frontend", "vue", "javascript"], "Vue");
tagMap.set(["frontend", "angular", "typescript"], "Angular");
tagMap.set(["backend", "node", "javascript"], "Node.js");
tagMap.set(["backend", "django", "python"], "Django");
// Query for frontend JavaScript frameworks
const jsFrameworks = tagMap.queryIndexedWith(["frontend", "javascript"]);
// [{ key: ["frontend", "react", "javascript"], value: "React" }, { key: ["frontend", "vue", "javascript"], value: "Vue" }]
// All frontend tech
const frontendTech = tagMap.queryIndexedWith(["frontend"]);
// Includes React, Vue, Angularimport { QueryableOrderedMultiKeyMap } from 'associum';
const pathMap = new QueryableOrderedMultiKeyMap<string[], string>();
pathMap.set(["api", "v1", "users", "GET"], "Get all users");
pathMap.set(["api", "v1", "users", "POST"], "Create user");
pathMap.set(["api", "v1", "users", ":id", "GET"], "Get user by ID");
pathMap.set(["api", "v1", "posts", "GET"], "Get all posts");
pathMap.set(["api", "v1", "posts", "POST"], "Create post");
// All user endpoints (prefix match)
const userEndpoints = pathMap.query(["api", "v1", "users"]);
// [{ key: ["api", "v1", "users", "GET"], value: "Get all users" }, ... , { key: ["api", "v1", "users", ":id", "GET"], value: "Get user by ID" }]
// All GET endpoints under v1 (positions 0,1,3)
const getEndpoints = pathMap.query(["api", "v1", undefined, "GET"]);
// Matches ["api", "v1", "users", "GET"], ["api", "v1", "posts", "GET"] (note: :id example requires adjustment for length)Note: For variable-length keys, wildcards work on specified positions; shorter keys may not match trailing wildcards.
import { QueryableStructuredMultiKeyMap } from 'associum';
const productMap = new QueryableStructuredMultiKeyMap<{
category: string,
subcategory: string,
brand: string,
model: string
}, number>();
// Set values
productMap.set({category: "electronics", subcategory: "phones", brand: "Apple", model: "iPhone 13"}, 999);
productMap.set({category: "electronics", subcategory: "phones", brand: "Samsung", model: "Galaxy S21"}, 899);
productMap.set({category: "electronics", subcategory: "laptops", brand: "Apple", model: "MacBook Pro"}, 1999);
productMap.set({category: "clothing", subcategory: "shirts", brand: "Nike", model: "Sport Shirt"}, 49);
// Query for all Apple products
const appleProducts = productMap.query({brand: "Apple"});
// Returns entries for iPhone 13 and MacBook Pro
// Query for all electronic phones
const phones = productMap.query({category: "electronics", subcategory: "phones"});
// Returns entries for iPhone 13 and Galaxy S21A multi-key map where the order of elements in the key array doesn't matter.
const map = new UnorderedMultiKeyMap<string[], number>();A multi-key map where the order of elements in the key array matters.
const map = new OrderedMultiKeyMap<string[], number>();A multi-key map that uses objects as keys, where the first level of properties acts as the key.
const map = new StructuredMultiKeyMap<{user: string, role: string}, number>();A queryable version of UnorderedMultiKeyMap that supports partial key lookups.
const map = new QueryableUnorderedMultiKeyMap<string[], number>();A queryable version of OrderedMultiKeyMap that supports partial key lookups.
const map = new QueryableOrderedMultiKeyMap<string[], number>();A queryable version of StructuredMultiKeyMap that supports partial key lookups.
const map = new QueryableStructuredMultiKeyMap<{user: string, role: string}, number>();All map classes extend the built-in JavaScript Map class and provide the following methods:
Sets a value for the specified key.
map.set(["A", "B"], 1);
map.set({user: "u1", role: "admin"}, 1);Returns the value associated with the specified key, or undefined if the key doesn't exist.
const value = map.get(["A", "B"]);
const value = map.get({user: "u1", role: "admin"});Returns a boolean indicating whether an element with the specified key exists.
const exists = map.has(["A", "B"]);
const exists = map.has({user: "u1", role: "admin"});Removes the element with the specified key. Returns true if an element existed and has been removed, or false if the element does not exist.
const deleted = map.delete(["A", "B"]);
const deleted = map.delete({user: "u1", role: "admin"});Removes all elements from the map.
map.clear();Returns a new iterator object that contains the keys for each element in the map.
for (const key of map.keys()) {
console.log(key);
}Returns a new iterator object that contains [key, value] pairs for each element in the map.
for (const [key, value] of map.entries()) {
console.log(key, value);
}Returns a new iterator object that contains the values for each element in the map.
for (const value of map.values()) {
console.log(value);
}Returns an array of entries that match the partial key template.
For OrderedMultiKeyMap:
const results = map.query(["A", undefined]); // Matches keys where first element is "A"For StructuredMultiKeyMap:
const results = map.query({user: "u1"}); // Matches keys where user property is "u1"Returns an array of entries that contain all of the specified key values, regardless of their position (UnorderedMultiKeyMap only).
const results = map.queryIndexedWith(["A", "B"]); // Matches keys containing both "A" and "B"MIT