tuple-ts 0.4.0 → 0.4.1

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 CHANGED
@@ -1,161 +1,330 @@
1
- # TupleTS
1
+ # tuple-ts
2
2
 
3
- TupleTS implements tuples and structs with shallow equality and identity semantics in TypeScript/JavaScript. It is designed to be tiny, fast, and memory-safe.
3
+ **Immutable tuples and structs with value-based identity for JavaScript and TypeScript.**
4
4
 
5
- - **Equality and Identity Semantics (`===` & `Object.is()`)**: Identical tuples and structs map to the same instance in memory.
6
- - **Composite Keys for `Set` and `Map`**: Use tuples or structs as keys without serialization or hashing workarounds.
7
- - **Memory-Safe**: No memory leaks ever due to advanced integration with garbage collection.
8
- - **Deeply Nestable & Immutable**: Tuples and structs can contain other tuples and structs.
9
- - **TypeScript First**: Full type inference.
10
- - **Customisable Sub-types**: Easily create your own custom tuple and struct sub-types.
11
- - **Zero Dependencies**: Lightweight implementation with fast hashing and no dependencies.
5
+ JavaScript normally compares arrays and objects by reference:
12
6
 
13
- ---
7
+ ```ts
8
+ [1, 2] === [1, 2] // false
9
+
10
+ { x: 1 } === { x: 1 } // false
11
+ ```
12
+
13
+ `tuple-ts` gives you a different model:
14
14
 
15
- ## Installation
15
+ ```ts
16
+ tuple(1, 2) === tuple(1, 2) // true
17
+
18
+ struct({ x: 1 }) === struct({ x: 1 }) // true
19
+ ```
20
+
21
+ Equal values are **interned to the same object**.
22
+
23
+ That makes tuples and structs immutable, naturally usable as composite `Map` keys and `Set` values, and particularly pleasant to use in functional TypeScript.
24
+
25
+ ## Install
16
26
 
17
27
  ```bash
18
28
  npm install tuple-ts
19
29
  ```
20
30
 
21
- ---
31
+ ## Tuples
32
+
33
+ Create an immutable tuple with full TypeScript inference:
34
+
35
+ ```ts
36
+ import { tuple } from "tuple-ts";
37
+
38
+ const point = tuple(10, 20);
39
+ // ^? readonly [10, 20]
40
+
41
+ console.log(point[0]); // 10
42
+ console.log(point[1]); // 20
43
+ ```
44
+
45
+ Calling `tuple` again with the same values gives you the **same object**:
22
46
 
23
- ## Usage Examples
47
+ ```ts
48
+ const a = tuple(10, 20);
49
+ const b = tuple(10, 20);
24
50
 
25
- ### 1. Basic Tuples, Identity and Equality
51
+ a === b; // true
52
+ Object.is(a, b); // true
53
+ ```
26
54
 
27
- Tuples with the same elements evaluate to the exact same object reference:
55
+ Different values give you different objects:
28
56
 
29
57
  ```ts
30
- import { tuple } from "tuple-ts";
58
+ tuple(10, 20) === tuple(10, 21); // false
59
+ tuple(10, 20) === tuple(20, 10); // false
60
+ ```
31
61
 
32
- const a = tuple(1, "hello", true);
33
- const b = tuple(1, "hello", true);
62
+ Tuples are ordinary frozen arrays, so there is no special API to learn:
34
63
 
35
- console.log(a === b); // true
36
- console.log(Object.is(a, b)); // true
64
+ ```ts
65
+ const t = tuple("hello", 42, true);
37
66
 
38
- // Tuples are standard frozen arrays:
39
- console.log(a[0]); // 1
40
- console.log(a.length); // 3
41
- console.log([...a]); // [1, "hello", true]
42
- console.log(Object.isFrozen(a)); // true
67
+ t.length; // 3
68
+ t[0]; // "hello"
69
+ [...t]; // ["hello", 42, true]
70
+ Object.isFrozen(t); // true
43
71
  ```
44
72
 
45
- ### 2. Nested Tuples
73
+ ## Why is this useful?
46
74
 
47
- Tuples can be nested arbitrarily and maintain referential equality across structures:
75
+ ### Composite `Map` keys
76
+
77
+ JavaScript doesn't have value-based arrays, so this doesn't work:
48
78
 
49
79
  ```ts
50
- import { tuple } from "tuple-ts";
80
+ const map = new Map();
51
81
 
52
- const t1 = tuple("matrix", tuple(1, 0), tuple(0, 1));
53
- const t2 = tuple("matrix", tuple(1, 0), tuple(0, 1));
54
- const t3 = tuple("matrix", tuple(1, 1), tuple(0, 1));
82
+ map.set([10, 20], "visited");
55
83
 
56
- console.log(t1 === t2); // true
57
- console.log(t1 === t3); // false
84
+ map.get([10, 20]); // undefined
58
85
  ```
59
86
 
60
- ### 3. Composite Keys in `Map` and `Set`
61
-
62
- In standard JavaScript, arrays and objects compare by reference, preventing them from being used as composite keys in `Map` or values in `Set`. With `tuple`, structural equality makes composite keys work seamlessly:
87
+ With `tuple-ts`:
63
88
 
64
89
  ```ts
65
- import { tuple } from "tuple-ts";
90
+ const map = new Map();
91
+
92
+ map.set(tuple(10, 20), "visited");
93
+
94
+ map.get(tuple(10, 20)); // "visited"
95
+ ```
96
+
97
+ The tuple constructed during the lookup is the same canonical object as the tuple used as the key.
98
+
99
+ This is particularly useful for things like coordinates, memoization keys, graph nodes, dynamic-programming states, and other composite values.
100
+
101
+ ### `Set` of values
66
102
 
67
- // Set: Deduplicating coordinate pairs
103
+ The same applies to `Set`:
104
+
105
+ ```ts
68
106
  const visited = new Set();
107
+
69
108
  visited.add(tuple(10, 20));
70
109
 
71
- console.log(visited.has(tuple(10, 20))); // true!
72
- console.log(visited.has(tuple(10, 21))); // false
110
+ visited.has(tuple(10, 20)); // true
111
+ visited.has(tuple(10, 21)); // false
112
+ ```
113
+
114
+ No serialization, stringification, or custom hashing is required.
115
+
116
+ ## Nested tuples
73
117
 
74
- // Map: Multi-argument function memoization / 2D grid
75
- const grid = new Map();
76
- grid.set(tuple(0, 0), "Origin");
77
- grid.set(tuple(3, 4), "Target");
118
+ Tuples can be nested:
78
119
 
79
- console.log(grid.get(tuple(0, 0))); // "Origin"
80
- console.log(grid.get(tuple(3, 4))); // "Target"
120
+ ```ts
121
+ const a = tuple(
122
+ "matrix",
123
+ tuple(1, 0),
124
+ tuple(0, 1),
125
+ );
126
+
127
+ const b = tuple(
128
+ "matrix",
129
+ tuple(1, 0),
130
+ tuple(0, 1),
131
+ );
132
+
133
+ a === b; // true
81
134
  ```
82
135
 
83
- ### 4. Custom Typed Tuples
136
+ Because nested tuples themselves have value-based identity, the resulting structures are canonicalised naturally.
137
+
138
+ ```ts
139
+ const a = tuple("a", tuple("b", "c"));
140
+ const b = tuple("a", tuple("b", "c"));
141
+ const c = tuple("a", tuple("b", "d"));
84
142
 
85
- You can easily create custom tuple sub-types:
143
+ a === b; // true
144
+ a === c; // false
145
+ ```
146
+
147
+ ## Typed tuples
148
+
149
+ You can create a reusable constructor for a particular tuple type:
86
150
 
87
151
  ```ts
88
- import { tuple } from "tuple-ts";
152
+ type Point = [x: number, y: number];
89
153
 
90
- type Point2D = [x: number, y: number];
91
- const Point2D = tuple<Point2D>;
154
+ const Point = tuple<Point>;
92
155
 
93
- const p1 = Point2D(12, 34);
94
- const p2 = Point2D(12, 34);
156
+ const a = Point(10, 20);
157
+ const b = Point(10, 20);
95
158
 
96
- console.log(p1 === p2); // true
97
- console.log(Object.is(p1, p2)); // true
159
+ a === b; // true
98
160
  ```
99
161
 
100
- ### 5. Structs
162
+ The result retains the tuple's TypeScript type while gaining the same runtime identity semantics.
101
163
 
102
- Structs with the same keys and values evaluate to the exact same frozen object reference:
164
+ ## Structs
165
+
166
+ `struct` provides the same semantics for objects:
103
167
 
104
168
  ```ts
105
169
  import { struct } from "tuple-ts";
106
170
 
107
- const userA = struct({ id: 1, role: "admin" });
108
- const userB = struct({ role: "admin", id: 1 }); // different key order
171
+ const a = struct({
172
+ id: 1,
173
+ role: "admin",
174
+ });
175
+
176
+ const b = struct({
177
+ role: "admin",
178
+ id: 1,
179
+ });
109
180
 
110
- console.log(userA === userB); // true
111
- console.log(Object.is(userA, userB)); // true
112
- console.log(userA.role); // "admin"
113
- console.log(Object.isFrozen(userA)); // true
181
+ a === b; // true
114
182
  ```
115
183
 
116
- ### 6. Custom Typed Structs
184
+ Property order does not matter.
117
185
 
118
- As with tuples, you can easily create custom struct sub-types:
186
+ Structs are frozen:
119
187
 
120
188
  ```ts
121
- import { struct } from "tuple-ts";
189
+ Object.isFrozen(a); // true
190
+ ```
191
+
192
+ And they work naturally as `Map` keys:
122
193
 
194
+ ```ts
195
+ const users = new Map();
196
+
197
+ users.set(
198
+ struct({ id: 42, role: "admin" }),
199
+ "Alice",
200
+ );
201
+
202
+ users.get(
203
+ struct({ role: "admin", id: 42 }),
204
+ ); // "Alice"
205
+ ```
206
+
207
+ ### Typed structs
208
+
209
+ As with tuples, you can define a reusable typed constructor:
210
+
211
+ ```ts
123
212
  interface User {
124
213
  id: string;
125
214
  name: string;
126
215
  updatedAt: number;
127
216
  }
217
+
128
218
  const User = struct<User>;
129
219
 
130
- const user1 = User({ id: "alice", name: "Alice", updatedAt: 1000 });
131
- const user2 = User({ name: "Alice", id: "alice", updatedAt: 1000 });
220
+ const a = User({
221
+ id: "alice",
222
+ name: "Alice",
223
+ updatedAt: 1000,
224
+ });
225
+
226
+ const b = User({
227
+ name: "Alice",
228
+ id: "alice",
229
+ updatedAt: 1000,
230
+ });
132
231
 
133
- console.log(user1 === user2); // true
134
- console.log(user1.name); // "Alice"
232
+ a === b; // true
135
233
  ```
136
234
 
137
- ### 7. Memory Safety & Garbage Collection
235
+ ## Equality semantics
138
236
 
139
- TupleTS uses `WeakRef` and `FinalizationRegistry` under the hood. When a tuple or struct is no longer referenced anywhere in your program, the JavaScript runtime garbage-collects it, and its entry in the intern cache is automatically pruned.
237
+ Primitive values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
140
238
 
141
239
  ```ts
142
- // No need to manually clear caches or worry about memory leaks.
143
- {
144
- const temp = tuple("transient", 123);
145
- // once 'temp' leaves scope and GC runs, the intern cache entry is freed.
146
- }
240
+ tuple(NaN) === tuple(NaN); // true
241
+
242
+ tuple(+0) === tuple(-0); // false
243
+ ```
244
+
245
+ Object values retain JavaScript's normal identity semantics.
246
+
247
+ The same object is the same value:
248
+
249
+ ```ts
250
+ const value = { x: 1 };
251
+
252
+ tuple(value) === tuple(value); // true
147
253
  ```
148
254
 
149
- ---
255
+ Two separately created objects are different values:
256
+
257
+ ```ts
258
+ tuple({ x: 1 }) === tuple({ x: 1 }); // false
259
+ ```
260
+
261
+ This is **shallow equality**. `tuple-ts` does not recursively compare arbitrary JavaScript objects.
262
+
263
+ Nested tuples and structs work particularly well because they are themselves canonical values.
264
+
265
+ ## Immutable by design
266
+
267
+ Tuples and structs returned by `tuple-ts` are frozen:
268
+
269
+ ```ts
270
+ const point = tuple(10, 20);
271
+
272
+ Object.isFrozen(point); // true
273
+ ```
274
+
275
+ This is important to the identity semantics: once a value has been interned, its contents cannot subsequently change while it is being used as a `Map` key or `Set` member.
276
+
277
+ ## Garbage-collection friendly
278
+
279
+ Interning values creates a natural question: *doesn't the cache grow forever?*
280
+
281
+ `tuple-ts` uses `WeakRef` and `FinalizationRegistry` internally. Cached values are weakly referenced, allowing the JavaScript garbage collector to reclaim values that are no longer referenced by the application.
282
+
283
+ In other words, you don't have to manually maintain a global cache of every tuple you've ever created.
284
+
285
+ ## TypeScript first
286
+
287
+ `tuple` and `struct` are designed to preserve TypeScript's inference:
288
+
289
+ ```ts
290
+ const value = tuple(
291
+ "hello",
292
+ 42,
293
+ true,
294
+ );
295
+
296
+ // readonly ["hello", 42, true]
297
+ ```
298
+
299
+ The library is deliberately small: the runtime API consists of the two constructors you actually need:
300
+
301
+ ```ts
302
+ import { tuple, struct } from "tuple-ts";
303
+ ```
304
+
305
+ ## When should I use it?
306
+
307
+ `tuple-ts` is particularly useful when you want **small immutable values with stable identity**:
308
+
309
+ * composite `Map` keys
310
+ * `Set` membership
311
+ * memoization keys
312
+ * graph/search states
313
+ * coordinates and other multi-dimensional values
314
+ * functional programming
315
+ * immutable application state
316
+ * canonical representations of frequently repeated values
317
+
318
+ If you just need an ordinary mutable array or object, use one. `tuple-ts` is for when you want the thing to behave more like a **value**.
319
+
320
+ ## Design
321
+
322
+ The implementation uses hashing to locate candidate values and then performs an exact shallow equality check before reusing an existing value. Hash collisions therefore do not determine equality.
323
+
324
+ The intern cache uses weak references, so canonical values do not need to be kept alive solely because they have previously been created.
325
+
326
+ There are no runtime dependencies.
150
327
 
151
- ## Equality Semantics
328
+ ## License
152
329
 
153
- TupleTS compares primitives via `Object.is`:
154
- - `NaN` is equivalent to `NaN` (`tuple(NaN) === tuple(NaN)`).
155
- - `+0` and `-0` are treated as distinct values (`tuple(+0) !== tuple(-0)`).
156
- - Object elements are compared by object instance identity:
157
- ```ts
158
- const shared = { value: 1 };
159
- tuple(shared) === tuple(shared); // true
160
- tuple({}) !== tuple({}); // true (different object instances)
161
- ```
330
+ ISC
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,CAAC;AAGvB,eAAO,MAAM,WAAW,yCAGvB,CAAC;AACF,eAAO,MAAM,YAAY,8CAGxB,CAAC;AA4BF,eAAO,MAAM,KAAK,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,gBAC9B,CAAC;AAEpC,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAE7D,CAAC,gBAAuC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,CAAC;AA6BvB,eAAO,MAAM,WAAW,yCAGvB,CAAC;AAEF,eAAO,MAAM,YAAY,8CAGxB,CAAC;AAEF,eAAO,MAAM,KAAK,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,gBAC9B,CAAC;AAEpC,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,gBACvC,CAAC"}
package/dist/index.js CHANGED
@@ -1,8 +1,6 @@
1
1
  import InternCache from "./intern-cache";
2
2
  export { InternCache };
3
3
  import * as hash from "./hash";
4
- export const _tupleCache = new InternCache(hash.tupleHash, arrayEquivalent);
5
- export const _structCache = new InternCache(hash.structHash, structEquivalent);
6
4
  function arrayEquivalent(a1, a2) {
7
5
  if (a2.length !== a1.length)
8
6
  return false;
@@ -26,5 +24,7 @@ function structEquivalent(r1, r2) {
26
24
  }
27
25
  return true;
28
26
  }
27
+ export const _tupleCache = new InternCache(hash.tupleHash, arrayEquivalent);
28
+ export const _structCache = new InternCache(hash.structHash, structEquivalent);
29
29
  export const tuple = (...t) => Object.freeze(_tupleCache.get(t));
30
30
  export const struct = ({ ...r }) => Object.freeze(_structCache.get(r));
@@ -25,4 +25,11 @@ describe("struct", () => {
25
25
  const b = geocode({ lat: 1, long: 2 });
26
26
  expect(a).toBe(b);
27
27
  });
28
+ it("returns frozen instances", () => {
29
+ const s = struct({ x: 1, y: 2 });
30
+ expect(Object.isFrozen(s)).toBe(true);
31
+ expect(() => {
32
+ s.x = 42;
33
+ }).toThrow();
34
+ });
28
35
  });
@@ -93,3 +93,10 @@ test("Tuple cache entry is cleared when tuple is garbage-collected", async () =>
93
93
  await waitForGC();
94
94
  expect(_tupleCache.size).toBe(0);
95
95
  });
96
+ test("Tuples are frozen", () => {
97
+ const t = tuple(1, 2, 3);
98
+ expect(Object.isFrozen(t)).toBe(true);
99
+ expect(() => {
100
+ t[0] = 42;
101
+ }).toThrow();
102
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "tuple-ts",
4
- "version": "0.4.0",
4
+ "version": "0.4.1",
5
5
  "description": "Tuples and structs for TypeScript and JavaScript with equality and identity semantics.",
6
6
  "keywords": [
7
7
  "tuple",