@vixsonous/map-array 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,201 @@
1
+ # MapArray
2
+
3
+ A TypeScript utility class that converts arrays into an O(1) lookup HashMap while keeping them fully loopable. No dependencies, lightweight, and TypeScript-first.
4
+
5
+ ## Why MapArray?
6
+
7
+ When working with arrays of objects, lookups like `array.find(item => item.id === id)` are O(N) — they scan the entire array every time. MapArray converts your array into a HashMap on initialization (O(N) once), then every subsequent lookup is O(1).
8
+
9
+ ```ts
10
+ // ❌ Without MapArray — O(N) every lookup
11
+ const user = users.find(u => u.id === "42");
12
+
13
+ // ✅ With MapArray — O(1) every lookup
14
+ const user = userMap.get("42");
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @vixsonous/map-array
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Array of Objects
26
+
27
+ ```ts
28
+ import { MapArray } from "@vixsonous/map-array";
29
+
30
+ interface User {
31
+ id: number;
32
+ name: string;
33
+ age: number;
34
+ }
35
+
36
+ const users = [
37
+ { id: 1, name: "Alice", age: 20 },
38
+ { id: 2, name: "Bob", age: 24 },
39
+ { id: 3, name: "Charlie", age: 26 },
40
+ ];
41
+
42
+ const userMap = new MapArray({ array: users, idField: "id" });
43
+
44
+ userMap.get("1"); // { id: 1, name: "Alice", age: 20 }
45
+ userMap.has("2"); // true
46
+ userMap.has("99"); // false
47
+ ```
48
+
49
+ ### Primitive Array
50
+
51
+ ```ts
52
+ const fruits = new MapArray({ array: ["apple", "banana", "cake"] });
53
+
54
+ fruits.has("apple"); // true
55
+ fruits.has("grape"); // false
56
+ ```
57
+
58
+ > **Note:** Primitive arrays are automatically deduplicated since values are used as keys.
59
+
60
+ ## API
61
+
62
+ ### Constructor
63
+
64
+ ```ts
65
+ new MapArray({ array?, idField? })
66
+ ```
67
+
68
+ | Parameter | Type | Description |
69
+ |-----------|------|-------------|
70
+ | `array` | `Array<T>` | Optional. The array to convert. |
71
+ | `idField` | `keyof T` | Required if `T` is an object. The field to use as the HashMap key. Must be a `string` or `number` field. |
72
+
73
+ > TypeScript will enforce `idField` at compile time
74
+
75
+ ---
76
+
77
+ ### `get(id)`
78
+ Returns the item with the given ID, or `undefined` if not found.
79
+ ```ts
80
+ userMap.get("1"); // { id: 1, name: "Alice", age: 20 }
81
+ userMap.get("99"); // undefined
82
+ ```
83
+
84
+ ---
85
+
86
+ ### `has(id)`
87
+ Returns `true` if an item with the given ID exists.
88
+ ```ts
89
+ userMap.has("1"); // true
90
+ userMap.has("99"); // false
91
+ ```
92
+
93
+ ---
94
+
95
+ ### `add(value, id?)`
96
+ Adds a new item. Optionally pass a custom ID.
97
+ ```ts
98
+ userMap.add({ id: 4, name: "Diana", age: 30 });
99
+ userMap.add({ id: 5, name: "Eve", age: 28 }, "custom-key");
100
+ ```
101
+
102
+ ---
103
+
104
+ ### `remove(id)`
105
+ Removes the item with the given ID.
106
+ ```ts
107
+ userMap.remove("1");
108
+ userMap.has("1"); // false
109
+ ```
110
+
111
+ ---
112
+
113
+ ### `set(id, field, value)`
114
+ Updates a specific field on an existing item. Type-safe — value must match the field's type.
115
+ ```ts
116
+ userMap.set("2", "name", "Bobby");
117
+ userMap.get("2"); // { id: 2, name: "Bobby", age: 24 }
118
+ ```
119
+
120
+ ---
121
+
122
+ ### `map(callback)`
123
+ Works like `Array.map()` — iterates over all items and returns a new array.
124
+ ```ts
125
+ const names = userMap.map(user => user.name);
126
+ // ["Alice", "Bob", "Charlie"]
127
+ ```
128
+
129
+ ---
130
+
131
+ ### `forEach(callback)`
132
+ Works like `Array.forEach()` — iterates over all items.
133
+ ```ts
134
+ userMap.forEach((user, index) => console.log(index, user.name));
135
+ ```
136
+
137
+ ---
138
+
139
+ ### `findMap(ids, callback)`
140
+ The standout feature. Maps only over a **subset of IDs** instead of the entire collection — O(K) where K is the number of IDs, instead of O(N).
141
+ ```ts
142
+ const result = userMap.findMap(["1", "3"], user => user.name);
143
+ // ["Alice", "Charlie"] — Bob is skipped entirely
144
+ ```
145
+
146
+ Also works with a single ID:
147
+ ```ts
148
+ userMap.findMap("1", user => user.name);
149
+ // ["Alice"]
150
+ ```
151
+
152
+ ---
153
+
154
+ ### `toArray()`
155
+ Converts the MapArray back to a plain array, reflecting the current state (including any additions or removals).
156
+ ```ts
157
+ userMap.remove("1");
158
+ userMap.toArray(); // [{ id: 2, ... }, { id: 3, ... }]
159
+ ```
160
+
161
+ ---
162
+
163
+ ### `getIds()`
164
+ Returns all current keys as an array of strings.
165
+ ```ts
166
+ userMap.getIds(); // ["1", "2", "3"]
167
+ ```
168
+
169
+ ---
170
+
171
+ ### `getLength()`
172
+ Returns the number of items.
173
+ ```ts
174
+ userMap.getLength(); // 3
175
+ ```
176
+
177
+ ---
178
+
179
+ ### Iterable
180
+ MapArray supports `for...of` natively:
181
+ ```ts
182
+ for (const user of userMap) {
183
+ console.log(user.name);
184
+ }
185
+ ```
186
+
187
+ ## Immutability Note
188
+
189
+ MapArray performs a deep clone (`structuredClone`) of your array on initialization, so mutations to the original array won't affect the MapArray and vice versa.
190
+
191
+ ```ts
192
+ const arr = [{ id: 1, name: "Alice" }];
193
+ const map = new MapArray({ array: arr, idField: "id" });
194
+
195
+ arr[0].name = "Bob";
196
+ map.get("1")?.name; // Still "Alice" ✅
197
+ ```
198
+
199
+ ## License
200
+
201
+ MIT
@@ -0,0 +1,28 @@
1
+ type FieldId<T> = {
2
+ [TKey in keyof T]: T[TKey] extends string | number ? TKey : never;
3
+ }[keyof T];
4
+ type MapArrayParametersType<T> = T extends string | number | boolean ? {
5
+ array?: Array<T> | undefined;
6
+ idField?: never;
7
+ } : {
8
+ array?: Array<T> | undefined;
9
+ idField: FieldId<T>;
10
+ };
11
+ export declare class MapArray<T> {
12
+ private readonly hashMap;
13
+ private readonly idField?;
14
+ constructor({ array, idField }: MapArrayParametersType<T>);
15
+ get(id: string | number): T | undefined;
16
+ has(id: string | number): boolean;
17
+ add(value: T, id?: string): void;
18
+ remove(id: string): void;
19
+ set<K extends keyof T>(id: string, field: K, value: T[K]): void;
20
+ map<TData>(callback: (item: T, index: number) => TData): Array<TData>;
21
+ forEach(callback: (item: T, index: number) => void): void;
22
+ findMap<TData>(ids: string | Array<string>, callback: (item: T, index: number) => TData): TData[];
23
+ getIds(): Array<string>;
24
+ getLength(): number;
25
+ [Symbol.iterator](): MapIterator<T>;
26
+ toArray(): Array<T>;
27
+ }
28
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,95 @@
1
+ /* Map Array is for object arrays, can also be used for single dimensional arrays but uses their value as accessor instead of index
2
+ * On initialization, perform O(N) operation to transform array into a HashMap
3
+ * On Item lookup, perform O(1) operation by using the idField as the field lookup
4
+ * The items in the hashmap is mutable
5
+ * If the specified array is a one dimensional array of primary data types, this will create a unique array
6
+ * If the specified array is a one dimensional array of objects, the uniqueness of the array will depend on the idField specified
7
+ * */
8
+ export class MapArray {
9
+ constructor({ array = [], idField = undefined }) {
10
+ this.hashMap = new Map();
11
+ this.idField = undefined;
12
+ const clonedArray = structuredClone(array);
13
+ clonedArray.forEach((item) => {
14
+ if (item === undefined || item === null)
15
+ return;
16
+ if (typeof item === "object" && idField === undefined) {
17
+ throw new Error("if the items are objects, idField must be defined!");
18
+ }
19
+ if (idField && typeof item === "object" && !(idField in item)) {
20
+ throw new Error("The specified id field is not in the item!");
21
+ }
22
+ /* If the type of the item is an object, then it will use the value of the specified field as the key. Otherwise, it will use itself as the key*/
23
+ this.hashMap.set(typeof item === "object" && idField ? String(item[idField]) : String(item), item);
24
+ });
25
+ this.idField = idField;
26
+ }
27
+ /* Gets the item from the hash array using the ID as the hash key */
28
+ get(id) {
29
+ return this.hashMap.get(String(id));
30
+ }
31
+ /* Checks the hash array if it contains an item with the specified key */
32
+ has(id) {
33
+ return this.hashMap.has(String(id));
34
+ }
35
+ /* Adds a new item to the hash array
36
+ * Optional id parameter if custom ID is needed
37
+ * */
38
+ add(value, id) {
39
+ const field = this.idField;
40
+ if (typeof value === "object" && field === undefined) {
41
+ throw new Error("The value is an object but the idField is not defined!");
42
+ }
43
+ this.hashMap.set(id ? id : String(typeof value === "object" && value !== null && field ? value[field] : value), value);
44
+ }
45
+ remove(id) {
46
+ this.hashMap.delete(id);
47
+ }
48
+ /* Sets the value of the specified item key */
49
+ set(id, field, value) {
50
+ const item = this.hashMap.get(id);
51
+ if (item === undefined || item === null) {
52
+ throw new Error("The item to modify is undefined!");
53
+ }
54
+ item[field] = value;
55
+ }
56
+ /* This will call the map data */
57
+ map(callback) {
58
+ const items = [];
59
+ let mapCounter = 0;
60
+ this.hashMap.forEach((item) => {
61
+ const modifiedItem = callback(item, mapCounter++);
62
+ items.push(modifiedItem);
63
+ });
64
+ return items;
65
+ }
66
+ forEach(callback) {
67
+ let mapCounter = 0;
68
+ this.hashMap.forEach((item) => callback(item, mapCounter++));
69
+ }
70
+ /* This will perform a map operation only on a set of IDs K instead of looping through all array item N :: K < N */
71
+ findMap(ids, callback) {
72
+ const items = [];
73
+ const idsArray = Array.isArray(ids) ? [...ids] : [ids];
74
+ idsArray.forEach((id, idx) => {
75
+ const item = this.hashMap.get(id) ? callback(this.hashMap.get(id), idx) : null;
76
+ if (item === null)
77
+ return;
78
+ items.push(item);
79
+ });
80
+ return items;
81
+ }
82
+ /* Gets the keys of the hashmap and form into an array */
83
+ getIds() {
84
+ return Array.from(this.hashMap.keys());
85
+ }
86
+ getLength() {
87
+ return this.hashMap.size;
88
+ }
89
+ [Symbol.iterator]() {
90
+ return this.hashMap.values();
91
+ }
92
+ toArray() {
93
+ return Array.from(this.hashMap.values());
94
+ }
95
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { MapArray } from "./index";
3
+ const exampleObjectArray = [
4
+ { id: 1, name: "Alice", age: 20 },
5
+ { id: 2, name: "Bob", age: 24 },
6
+ { id: 3, name: "Charlie", age: 26 },
7
+ ];
8
+ const examplePrimitiveStringArray = ["apple", "banana", "cake"];
9
+ describe("MapArray", () => {
10
+ it('should create a MapArray from an array of objects', () => {
11
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
12
+ expect(mapArray.has("1")).toBe(true);
13
+ expect(mapArray.has("2")).toBe(true);
14
+ expect(mapArray.has("3")).toBe(true);
15
+ /* Non existent id */
16
+ expect(mapArray.has("4")).toBe(false);
17
+ expect(mapArray.get("1")).toStrictEqual({ id: 1, name: "Alice", age: 20 });
18
+ expect(mapArray.get("2")).toStrictEqual({ id: 2, name: "Bob", age: 24 });
19
+ expect(mapArray.get("3")).toStrictEqual({ id: 3, name: "Charlie", age: 26 });
20
+ });
21
+ it('should create a MapArray from a primitive array', () => {
22
+ const mapArray = new MapArray({ array: examplePrimitiveStringArray });
23
+ expect(mapArray.has("apple")).toBe(true);
24
+ expect(mapArray.has("banana")).toBe(true);
25
+ expect(mapArray.has("cake")).toBe(true);
26
+ expect(mapArray.has("Not existing item")).toBe(false);
27
+ });
28
+ it('should add an item', () => {
29
+ const mapArray = new MapArray({ idField: 'id' });
30
+ mapArray.add({ id: 4, name: "Danil", age: 50 });
31
+ expect(mapArray.has("4")).toBe(true);
32
+ });
33
+ it('should remove an item', () => {
34
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
35
+ mapArray.remove("3");
36
+ /* Map Array should not have the object with the "3" as ID anymore */
37
+ expect(mapArray.has("3")).toBe(false);
38
+ });
39
+ it('should map over items', () => {
40
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
41
+ const result = mapArray.map(item => item.name);
42
+ expect(result).toStrictEqual(exampleObjectArray.map(item => item.name));
43
+ });
44
+ it('should find and map over specific IDs', () => {
45
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
46
+ const idsToTarget = ["1", "2"]; /* Existing ids from the exampleObjectArray */
47
+ const result = mapArray.findMap(idsToTarget, (item) => item.name);
48
+ /* Expect to only map the objects that has the IDs 1 and 2 (from the target ids), leaving out "Charlie" */
49
+ expect(result).toStrictEqual(["Alice", "Bob"]);
50
+ });
51
+ it('should find and map a singular ID', () => {
52
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
53
+ const idToTarget = "1"; /* Existing ids from the exampleObjectArray */
54
+ const result = mapArray.findMap(idToTarget, (item) => item.name);
55
+ /* Expect to only map the objects that has the ID 1(from the target id), leaving out "Bob" and "Charlie" */
56
+ expect(result).toStrictEqual(["Alice"]);
57
+ });
58
+ it('should set a field value on an existing item in the array of objects', () => {
59
+ var _a;
60
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
61
+ const toModifyNameValue = "Victor";
62
+ mapArray.set("1", "name", toModifyNameValue);
63
+ expect((_a = mapArray.get("1")) === null || _a === void 0 ? void 0 : _a.name).toBe(toModifyNameValue);
64
+ });
65
+ it('should iterate over all items with forEach', () => {
66
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
67
+ const names = [];
68
+ mapArray.forEach(item => {
69
+ names.push(item.name);
70
+ });
71
+ expect(names).toStrictEqual(["Alice", "Bob", "Charlie"]);
72
+ });
73
+ it('should have an iterator to iterate throughout the map array', () => {
74
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
75
+ for (const item of mapArray) {
76
+ expect(mapArray.has(String(item.id))).toBe(true);
77
+ }
78
+ });
79
+ it('should convert the map array back to an array', () => {
80
+ const mapArray = new MapArray({ array: exampleObjectArray, idField: 'id' });
81
+ /* Should be similar to the original object array it came from*/
82
+ expect(mapArray.toArray()).toStrictEqual(exampleObjectArray);
83
+ const toModifyNameValue = "Victor";
84
+ mapArray.set("1", "name", toModifyNameValue);
85
+ /* It should convert to an array with the modified values */
86
+ expect(mapArray.toArray()).toStrictEqual([
87
+ { id: 1, name: toModifyNameValue, age: 20 },
88
+ { id: 2, name: "Bob", age: 24 },
89
+ { id: 3, name: "Charlie", age: 26 },
90
+ ]);
91
+ });
92
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@vixsonous/map-array",
3
+ "version": "1.0.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "test": "vitest run"
12
+ },
13
+ "keywords": [
14
+ "map",
15
+ "array",
16
+ "hashmap",
17
+ "typescript"
18
+ ],
19
+ "author": {
20
+ "name": "Victor Chiong",
21
+ "email": "chiong.vict@gmail.com",
22
+ "url": "https://victorchiong.com/"
23
+ },
24
+ "license": "MIT",
25
+ "description": "A typescript class that converts arrays into a loopable, O(1) lookup HashMap",
26
+ "devDependencies": {
27
+ "vitest": "^4.1.4",
28
+ "typescript": "^6.0.2"
29
+ }
30
+ }