tuple-ts 0.6.0 → 0.7.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,608 +1,306 @@
1
1
  # tuple-ts
2
2
 
3
- **Immutable tuples, records and structs with value-based identity for JavaScript and TypeScript.**
3
+ **TypeScript-first tuples and structs with value-based identity.**
4
4
 
5
- JavaScript normally compares arrays and objects by reference:
5
+ JavaScript normally compares arrays by reference:
6
6
 
7
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
-
15
- ```ts
16
- tuple(1, 2) === tuple(1, 2) // true
17
-
18
- record({ x: 1 }) === record({ x: 1 }) // true
19
- ```
20
-
21
- Equal values are **interned to the same object**.
22
-
23
- That makes tuples, records and structs immutable, naturally usable as composite `Map` keys and `Set` values, and particularly pleasant to use in functional TypeScript.
24
-
25
- **Zero dependencies · Less than 1 KB gzipped**
26
-
27
- ## Install
28
-
29
- ```bash
30
- npm install tuple-ts
8
+ [1, "foo"] === [1, "foo"]; // false!
31
9
  ```
32
10
 
33
- ## Tuples
34
-
35
- Create an immutable tuple with full TypeScript inference:
11
+ `tuple-ts` provides opt-in _value-based identity_:
36
12
 
37
13
  ```ts
38
14
  import { tuple } from "tuple-ts";
39
15
 
40
- const point = tuple(10, 20);
41
- // ^? readonly [10, 20]
42
-
43
- console.log(point[0]); // 10
44
- console.log(point[1]); // 20
16
+ tuple(1, "foo") === tuple(1, "foo"); // true
45
17
  ```
46
18
 
47
- Calling `tuple` again with the same values gives you the **same object**:
19
+ with optional slot labels:
48
20
 
49
21
  ```ts
50
- const a = tuple(10, 20);
51
- const b = tuple(10, 20);
22
+ import { struct } from "tuple-ts";
52
23
 
53
- a === b; // true
54
- Object.is(a, b); // true
55
- ```
24
+ const Person = struct(["name", "age"]);
56
25
 
57
- Different values give you different objects:
26
+ const person1 = Person("Bob", 25);
58
27
 
59
- ```ts
60
- tuple(10, 20) === tuple(10, 21); // false
61
- tuple(10, 20) === tuple(20, 10); // false
28
+ person1[0] === "Bob"; // true
29
+ person1.name === "Bob"; // true
62
30
  ```
63
31
 
64
- Tuples are ordinary frozen arrays, so there is no special API to learn:
32
+ and support for label-based construction:
65
33
 
66
34
  ```ts
67
- const t = tuple("hello", 42, true);
35
+ const person2 = Person.from({ name: "Bob", age: 25 });
68
36
 
69
- t.length; // 3
70
- t[0]; // "hello"
71
- [...t]; // ["hello", 42, true]
72
- Object.isFrozen(t); // true
37
+ person1 === person2; // true
73
38
  ```
74
39
 
75
- ## Why is this useful?
40
+ `tuple-ts` is useful when you want small immutable values with stable identity:
76
41
 
77
- ### Composite `Map` keys
42
+ - Composite `Map` and `Set` keys
43
+ - Memoization keys
44
+ - Graph and search states
45
+ - Coordinates and other multi-dimensional values
46
+ - Named, slot-based values such as points or pairs
78
47
 
79
- JavaScript doesn't have value-based arrays, so this doesn't work:
48
+ `tuple-ts` has zero dependencies and is less than 1 KB gzipped.
80
49
 
81
- ```ts
82
- const map = new Map();
83
-
84
- map.set([10, 20], "visited");
50
+ ## Install
85
51
 
86
- map.get([10, 20]); // undefined
52
+ ```bash
53
+ npm install tuple-ts
87
54
  ```
88
55
 
89
- With `tuple-ts`:
56
+ ## Tuples
90
57
 
91
- ```ts
92
- const map = new Map();
58
+ The values returned by `tuple(...)` are regular frozen JavaScript arrays:
93
59
 
94
- map.set(tuple(10, 20), "visited");
60
+ ```ts
61
+ const tuple1 = tuple(1, "foo");
95
62
 
96
- map.get(tuple(10, 20)); // "visited"
63
+ Array.isArray(tuple1); // true
64
+ tuple1.length === 2; // true
65
+ tuple1[0] === 1; // true
66
+ tuple1[1] === "foo"; // true
67
+ Object.isFrozen(tuple1); // true
68
+ [ ...tuple1 ]; // [1, "foo"]
97
69
  ```
98
70
 
99
- The tuple constructed during the lookup is the same canonical object as the tuple used as the key.
100
-
101
- This is particularly useful for things like coordinates, memoization keys, graph nodes, dynamic-programming states, and other composite values.
102
-
103
- ### `Set` of values
104
-
105
- The same applies to `Set`:
71
+ ...with automatically inferred literal types:
106
72
 
107
73
  ```ts
108
- const visited = new Set();
109
-
110
- visited.add(tuple(10, 20));
111
-
112
- visited.has(tuple(10, 20)); // true
113
- visited.has(tuple(10, 21)); // false
74
+ type Tuple1 = typeof tuple1; // readonly [1, "foo"]
114
75
  ```
115
76
 
116
- No serialization, stringification, or custom hashing is required.
117
-
118
- ## Nested tuples
119
-
120
- Tuples can be nested:
77
+ Type-narrowed tuple factories are also supported:
121
78
 
122
79
  ```ts
123
- const a = tuple(
124
- "matrix",
125
- tuple(1, 0),
126
- tuple(0, 1),
127
- );
128
-
129
- const b = tuple(
130
- "matrix",
131
- tuple(1, 0),
132
- tuple(0, 1),
133
- );
80
+ type Point = [number, number];
81
+ const Point = tuple.narrow<Point>();
82
+ // Or equivalently, using an exemplar:
83
+ // const Point = tuple.narrow(0, 0);
134
84
 
135
- a === b; // true
85
+ type P = Parameters<typeof Point>; // [number, number];
136
86
  ```
137
87
 
138
- Because nested tuples themselves have value-based identity, the resulting structures are canonicalised naturally.
88
+ Calling `tuple()` again with the same values returns the same canonical object:
139
89
 
140
90
  ```ts
141
- const a = tuple("a", tuple("b", "c"));
142
- const b = tuple("a", tuple("b", "c"));
143
- const c = tuple("a", tuple("b", "d"));
91
+ const a = tuple(10, 20);
92
+ const b = tuple(10, 20);
144
93
 
145
94
  a === b; // true
146
- a === c; // false
95
+ Object.is(a, b); // true
147
96
  ```
148
97
 
149
- ## Typed tuples
150
-
151
- Use `tuple.like()` to create a reusable constructor for a particular tuple type.
152
-
153
- ### Inferring the type from an exemplar
98
+ Different values or a different element order produce different tuples:
154
99
 
155
100
  ```ts
156
- const Point = tuple.like(0, 0);
157
-
158
- const a = Point(10, 20);
159
- const b = Point(10, 20);
160
-
161
- a === b; // true
101
+ tuple(10, 20) === tuple(10, 21); // false!
102
+ tuple(10, 20) === tuple(20, 10); // false!
162
103
  ```
163
104
 
164
- The exemplar determines the TypeScript type expected by the resulting factory. Its values are not used as defaults.
105
+ ### Constraining a tuple factory with `.narrow()`
165
106
 
166
- By default, TypeScript widens the exemplar's literal types, so `tuple.like(0, 0)` produces a factory accepting two numbers.
107
+ Use `.narrow()` to make a reusable tuple factory whose arguments must satisfy a nominated tuple type.
167
108
 
168
- ### Explicit type arguments
169
-
170
- Alternatively, specify the type directly:
109
+ Specify the type explicitly:
171
110
 
172
111
  ```ts
173
- type Point = [x: number, y: number];
174
-
175
- const Point = tuple.like<Point>();
176
-
177
- const a = Point(10, 20);
178
- const b = Point(10, 20);
112
+ const Point = tuple.narrow<[number, number]>();
179
113
 
180
- a === b; // true
114
+ Point(10, 20); // OK
115
+ Point(10, "20"); // TypeScript error
181
116
  ```
182
117
 
183
- No exemplar is required when the type argument is provided explicitly.
184
-
185
- ### Preserving literal types
186
-
187
- To preserve literal types rather than widen them, use `as const`:
118
+ You can alternatively provide an exemplar from which the type constraint will be inferred:
188
119
 
189
120
  ```ts
190
- const Direction = tuple.like("north" as const, 1 as const);
121
+ const NumberPair = tuple.narrow(0, 0);
191
122
 
192
- Direction("north", 1); // OK
193
- Direction("south", 1); // TypeScript error
123
+ NumberPair(10, 20); // OK
124
+ NumberPair(10, "20"); // TypeScript error
194
125
  ```
195
126
 
196
- In all cases, the resulting tuples are frozen and interned using the same shared cache as ordinary tuples.
197
127
 
198
- ## Records
128
+ ### Nesting tuples
199
129
 
200
- `record` provides the same value-based identity semantics for objects:
130
+ Tuples can contain other interned tuples:
201
131
 
202
132
  ```ts
203
- import { record } from "tuple-ts";
204
-
205
- const a = record({
206
- id: 1,
207
- role: "admin",
208
- });
209
-
210
- const b = record({
211
- role: "admin",
212
- id: 1,
213
- });
133
+ const a = tuple("a", tuple("b", "c"));
134
+ const b = tuple("a", tuple("b", "c"));
135
+ const c = tuple("a", tuple("b", "d"));
214
136
 
215
137
  a === b; // true
216
- ```
217
-
218
- Property order does not matter.
138
+ a === c; // false!
219
139
 
220
- Records are frozen:
140
+ const mat1 = tuple("matrix", tuple(1, 0), tuple(0, 1));
141
+ const mat2 = tuple("matrix", tuple(1, 0), tuple(0, 1));
221
142
 
222
- ```ts
223
- Object.isFrozen(a); // true
143
+ mat1 === mat2; // true
224
144
  ```
225
145
 
226
- And they work naturally as `Map` keys:
227
-
228
- ```ts
229
- const users = new Map();
230
-
231
- users.set(
232
- record({ id: 42, role: "admin" }),
233
- "Alice",
234
- );
146
+ ## Structs
235
147
 
236
- users.get(
237
- record({ role: "admin", id: 42 }),
238
- ); // "Alice"
239
- ```
148
+ Use `struct()` when you want a tuple with meaningful labels.
149
+ Provide a list of labels to create a reusable factory that produces tuples with additional named properties.
240
150
 
241
- ### Record identity
151
+ ```ts
152
+ import { struct } from "tuple-ts";
242
153
 
243
- A record's identity is determined by its own, enumerable, string-keyed properties and their values.
154
+ const Point = struct(["x", "y"]);
244
155
 
245
- Two records are equivalent if they contain exactly the same keys, with equivalent values, regardless of property order.
156
+ const p = Point(10, 20);
246
157
 
247
- ```ts
248
- record({ x: 1, y: 2 }) ===
249
- record({ y: 2, x: 1 }); // true
158
+ p[0]; // 10
159
+ p[1]; // 20
160
+ p.x; // 10
161
+ p.y; // 20
250
162
 
251
- record({ x: 1 }) ===
252
- record({ x: 1, y: 2 }); // false
163
+ Array.isArray(p); // true!
164
+ Object.isFrozen(p); // true
165
+ [ ...p ]; // [10, 20]
166
+ { ...p }; // { 0: 10, 1: 20, x: 10, y: 20 }
253
167
  ```
254
168
 
255
- Symbol-keyed, inherited and non-enumerable properties are not copied into records.
169
+ ### Constructing from an object with `.from()`
256
170
 
257
- Numeric property keys are supported and follow JavaScript's normal conversion to string keys.
258
-
259
- ### Typed records
260
-
261
- Use `record.like()` to create a reusable typed record constructor:
171
+ `.from()` accepts an object with the declared labels and creates the corresponding positional tuple:
262
172
 
263
173
  ```ts
264
- interface User {
265
- id: string;
266
- name: string;
267
- updatedAt: number;
268
- }
269
-
270
- const User = record.like<User>();
271
-
272
- const a = User({
273
- id: "alice",
274
- name: "Alice",
275
- updatedAt: 1000,
276
- });
277
-
278
- const b = User({
279
- name: "Alice",
280
- id: "alice",
281
- updatedAt: 1000,
282
- });
283
-
284
- a === b; // true
285
- ```
174
+ const Point = struct(["x", "y"]);
286
175
 
287
- Alternatively, infer the type from an exemplar:
176
+ const point1 = Point.from({ x: 10, y: 20 });
288
177
 
289
- ```ts
290
- const User = record.like({
291
- id: "",
292
- name: "",
293
- updatedAt: 0,
294
- });
178
+ point1 === Point(10, 20); // true
179
+ point1[0] === 10; // true
180
+ point1.x === 10; // true
295
181
  ```
296
182
 
297
- As with `tuple.like()`, the exemplar defines the type; it does not supply default values.
298
-
299
- The resulting factory requires the properties established by `like()`, but may also accept additional properties.
300
-
301
- Unlike `struct`, `record` preserves those additional properties, and they contribute to identity.
183
+ The values are read in the schema's label order, regardless of the input object's property order. Extra properties on the input object are ignored:
302
184
 
303
185
  ```ts
304
- const User = record.like({ id: "" });
186
+ const p = Point.from({ x: 10, y: 20, altitude: 100 });
305
187
 
306
- const a = User({ id: "alice" });
307
- const b = User({ id: "alice", role: "admin" });
308
-
309
- a === b; // false
188
+ p.x; // 10
189
+ p.y; // 20
310
190
  ```
311
191
 
312
- ## Structs
192
+ The returned value contains the tuple slots and the schema's named properties, not the unrelated input properties.
313
193
 
314
- `record` interns whatever properties you give it. If you want a fixed set of keys to define identity, use `struct`.
194
+ ### Constraining a struct factory
315
195
 
316
- `struct` takes key names as arguments and returns a factory. The factory accepts any object with at least those keys, extracts **exactly the declared keys**, and interns the result.
196
+ Just like `tuple`, structs support `.narrow()` to specify the struct's value types positionally:
317
197
 
318
198
  ```ts
319
- import { struct } from "tuple-ts";
320
-
321
- const Point = struct("x", "y");
322
-
323
- const a = Point({ x: 1, y: 2 });
324
- const b = Point({ x: 1, y: 2 });
199
+ const Point = struct(["x", "y"]).narrow<[number, number]>();
325
200
 
326
- a === b; // true
201
+ Point(10, 20); // OK
202
+ Point(10, "20"); // TypeScript error
327
203
  ```
328
204
 
329
- ### Additional properties are discarded
330
-
331
- Unlike `record`, a struct factory ignores properties that are not part of its declared structure:
205
+ Use `.narrowFrom()` when you want to express the value types by label instead:
332
206
 
333
207
  ```ts
334
- const Point = struct("x", "y");
208
+ type Coordinates = { x: number; y: number };
335
209
 
336
- const a = Point({ x: 1, y: 2 });
337
- const b = Point({ x: 1, y: 2, z: 3 });
210
+ const Point = struct(["x", "y"]).narrowFrom<Coordinates>();
338
211
 
339
- a === b; // true
212
+ Point(10, 20); // OK
213
+ Point(10, "20"); // TypeScript error
214
+ Point.from({ x: 10, y: 20 }); // OK
340
215
  ```
341
216
 
342
- The resulting objects contain only `x` and `y`:
217
+ You can also provide a typed exemplar to `.narrowFrom()`:
343
218
 
344
219
  ```ts
345
- Object.keys(b); // ["x", "y"]
346
-
347
- b.z; // TypeScript error
220
+ const Point = struct(["x", "y"]).narrowFrom({ x: 0, y: 0 });
348
221
  ```
349
222
 
350
- This makes structs useful for projecting larger objects into canonical values with a precisely defined shape.
223
+ ### Struct schemas and identity
351
224
 
352
- ```ts
353
- const Position = struct("x", "y");
225
+ Each call to `struct(labels)` creates its own cache. Factories narrowed from that schema share the cache, but separate calls to `struct()` create separate caches, even when given identical labels:
354
226
 
355
- const location = {
356
- x: 10,
357
- y: 20,
358
- altitude: 100,
359
- timestamp: Date.now(),
360
- };
227
+ ```ts
228
+ const PointA = struct(["x", "y"]).narrow<[number, number]>();
229
+ const PointB = struct(["x", "y"]).narrow<[number, number]>();
361
230
 
362
- const position = Position(location);
363
- // { x: 10, y: 20 }
231
+ PointA(1, 2) === PointA(1, 2); // true
232
+ PointA(1, 2) === PointB(1, 2); // false
364
233
  ```
365
234
 
366
- The factory is deliberately permissive about additional input properties, but strict about which properties appear in its output.
235
+ Struct identity is therefore scoped to the schema factory. Ordinary `tuple()` values use a separate, shared tuple cache and do not share identity with struct values, even when their indexed elements are equal.
367
236
 
368
- ### Typed structs
237
+ ## Tuples and structs as composite keys for `Map` and `Set`
369
238
 
370
- Use `struct(...keys).like()` to constrain the types of the declared properties.
239
+ `tuple` and `struct` let you use composite keys with JavaScript's built-in `Map` and `Set`, without serialization or stringification.
371
240
 
372
- The type can be inferred from an exemplar:
241
+ JavaScript's `Map` and `Set` treat separately constructed arrays as distinct keys, even when their contents are equal:
373
242
 
374
243
  ```ts
375
- const Point = struct("x", "y").like({
376
- x: 0,
377
- y: 0,
378
- });
379
-
380
- Point({ x: 10, y: 20 }); // OK
381
- Point({ x: "10", y: 20 }); // TypeScript error
382
- ```
244
+ const myMap = new Map();
245
+ myMap.set([10, 20], "visited");
383
246
 
384
- Or provided explicitly:
247
+ myMap.has([10, 20]); // false!
248
+ myMap.get([10, 20]); // undefined!
385
249
 
386
- ```ts
387
- type Coordinates = {
388
- x: number;
389
- y: number;
390
- };
391
-
392
- const Point = struct("x", "y").like<Coordinates>();
250
+ const mySet = new Set();
251
+ mySet.add([10, 20]);
393
252
 
394
- Point({ x: 10, y: 20 }); // OK
253
+ mySet.has([10, 20]); // false!
395
254
  ```
396
255
 
397
- As with the other factories, `like()` defines types, not default values.
398
-
399
- ### Strict definitions, flexible construction
400
-
401
- There is an intentional distinction between defining a struct and constructing one.
402
-
403
- A `like()` exemplar must contain only the keys declared by `struct`:
256
+ With `tuple-ts`:
404
257
 
405
258
  ```ts
406
- const Point = struct("x", "y");
259
+ const myMap = new Map();
260
+ myMap.set(tuple(10, 20), "visited");
407
261
 
408
- Point.like({
409
- x: 0,
410
- y: 0,
411
- z: 0, // TypeScript error: unexpected property
412
- });
413
- ```
414
-
415
- But the resulting factory accepts extra properties and discards them:
262
+ myMap.has(tuple(10, 20)); // true
263
+ myMap.get(tuple(10, 20)); // "visited"
416
264
 
417
- ```ts
418
- const Point = struct("x", "y").like({
419
- x: 0,
420
- y: 0,
421
- });
265
+ const Point = struct(["x", "y"]);
266
+ const mySet = new Set();
267
+ mySet.add(Point(1, 2));
422
268
 
423
- Point({ x: 10, y: 20, z: 30 }); // OK
424
- // { x: 10, y: 20 }
269
+ mySet.has(Point(1, 2)); // true
425
270
  ```
426
271
 
427
- This keeps struct definitions precise without making callers manually remove unrelated properties from their input objects.
428
-
429
272
  ## Equality semantics
430
273
 
431
- Values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
274
+ Tuple elements are compared shallowly using `Object.is`:
432
275
 
433
276
  ```ts
434
277
  tuple(NaN) === tuple(NaN); // true
435
-
436
278
  tuple(+0) === tuple(-0); // false
437
279
  ```
438
280
 
439
- Object values retain JavaScript's normal identity semantics.
440
-
441
- The same object is the same value:
281
+ Object-valued elements retain JavaScript's normal reference identity:
442
282
 
443
283
  ```ts
444
284
  const value = { x: 1 };
445
-
446
285
  tuple(value) === tuple(value); // true
447
- ```
448
286
 
449
- Two separately created objects are different values:
450
-
451
- ```ts
452
287
  tuple({ x: 1 }) === tuple({ x: 1 }); // false
453
288
  ```
454
289
 
455
- This is **shallow equality**. `tuple-ts` does not recursively compare arbitrary JavaScript objects.
456
-
457
- Nested tuples, records and structs work particularly well because they are themselves canonical values:
290
+ `tuple-ts` does not recursively compare arbitrary JavaScript objects. Nested tuples work well because the values are themselves tuples:
458
291
 
459
292
  ```ts
460
- const a = record({
461
- position: tuple(10, 20),
462
- });
463
-
464
- const b = record({
465
- position: tuple(10, 20),
466
- });
293
+ const a = tuple(tuple(1, 2));
294
+ const b = tuple(tuple(1, 2));
467
295
 
468
296
  a === b; // true
469
297
  ```
470
298
 
471
- The outer records compare equal because the inner tuples have already been interned to the same object.
472
-
473
- All three constructors share this model of identity.
474
-
475
- ## Immutable by design
476
-
477
- Tuples, records and structs returned by `tuple-ts` are frozen:
478
-
479
- ```ts
480
- const point = tuple(10, 20);
481
- const user = record({ id: 1 });
482
- const position = struct("x", "y")({ x: 1, y: 2 });
483
-
484
- Object.isFrozen(point); // true
485
- Object.isFrozen(user); // true
486
- Object.isFrozen(position); // true
487
- ```
488
-
489
- This is important to the identity semantics: once a value has been interned, its own properties cannot subsequently change while it is being used as a `Map` key or `Set` member.
490
-
491
- Freezing is shallow. If an interned value contains a reference to an ordinary mutable object, that nested object is not automatically frozen.
492
-
493
- For example:
494
-
495
- ```ts
496
- const details = { name: "Alice" };
497
- const user = record({ details });
498
-
499
- details.name = "Bob"; // Allowed
500
- ```
501
-
502
- The reference to `details` remains unchanged, so the interned record retains its identity.
503
-
504
- If you need immutable nested values, construct them using `tuple`, `record` or `struct` as well.
505
-
506
- ## Garbage-collection friendly
507
-
508
- Interning values creates a natural question: *doesn't the cache grow forever?*
509
-
510
- `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.
511
-
512
- In other words, you don't have to manually maintain a global cache of every tuple, record or struct you've ever created.
513
-
514
- ## TypeScript first
515
-
516
- `tuple`, `record` and `struct` are designed to preserve TypeScript's inference.
517
-
518
- ### Literal inference
519
-
520
- By default, constructors preserve literal types:
521
-
522
- ```ts
523
- const t = tuple("hello", 42, true);
524
- // readonly ["hello", 42, true]
525
-
526
- const r = record({ x: 1, y: 2 });
527
- // readonly properties x: 1, y: 2
528
- ```
529
-
530
- ### Reusable typed factories
531
-
532
- Use `.like()` to define reusable constructors:
533
-
534
- ```ts
535
- const Pair = tuple.like("", 0);
536
-
537
- const User = record.like({
538
- id: "",
539
- name: "",
540
- });
541
-
542
- const Point = struct("x", "y").like({
543
- x: 0,
544
- y: 0,
545
- });
546
- ```
547
-
548
- Or specify types explicitly:
549
-
550
- ```ts
551
- const Pair = tuple.like<[string, number]>();
552
-
553
- const User = record.like<{
554
- id: string;
555
- name: string;
556
- }>();
557
-
558
- const Point = struct("x", "y").like<{
559
- x: number;
560
- y: number;
561
- }>();
562
- ```
563
-
564
- In either form, TypeScript checks arguments to the resulting factories.
565
-
566
- ### Readonly results
567
-
568
- All constructors return readonly types, reflecting the frozen runtime objects.
569
-
570
- The readonly typing is shallow, just like `Object.freeze()`.
571
-
572
- ## When should I use it?
573
-
574
- `tuple-ts` is particularly useful when you want **small immutable values with stable identity**:
575
-
576
- - Composite `Map` keys
577
- - `Set` membership
578
- - Memoization keys
579
- - Graph/search states
580
- - Coordinates and other multi-dimensional values
581
- - Functional programming
582
- - Immutable application state
583
- - Canonical representations of frequently repeated values
584
-
585
- Choose between the three constructors according to your data:
586
-
587
- | Constructor | Shape | Identity |
588
- |---|---|---|
589
- | `tuple` | Ordered sequence | Position and value of each element |
590
- | `record` | Arbitrary object properties | Complete set of keys and values |
591
- | `struct` | Fixed declared keys | Values of the declared keys only |
592
-
593
- 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**.
594
-
595
- ## Design
596
-
597
- 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.
598
-
599
- Property order is ignored for records and structs, but element order matters for tuples.
600
-
601
- The intern cache uses weak references, so canonical values do not need to be kept alive solely because they have previously been created.
299
+ ## Garbage Collection
602
300
 
603
- Constructed records and structs are copied before interning, so callers cannot alter an interned value by modifying the source object.
301
+ `tuple-ts` works by maintaining internal caches of tuple/struct objects. This raises a natural question: *doesn't the cache grow forever?*
604
302
 
605
- There are no runtime dependencies.
303
+ The cache uses `WeakRef` to avoid keeping interned objects alive solely because they are cached. A `FinalizationRegistry` is used to clean up cache entries after objects are garbage-collected. This allows unreferenced values to be reclaimed without the cache retaining them indefinitely.
606
304
 
607
305
  ## License
608
306