tuple-ts 0.7.0 → 0.8.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 CHANGED
@@ -1,154 +1,133 @@
1
1
  # tuple-ts
2
2
 
3
- **Immutable interned tuples and named tuples for JavaScript and TypeScript.**
3
+ **TypeScript-first tuples and structs with value-based identity.**
4
4
 
5
- JavaScript normally compares arrays by reference:
5
+ JavaScript normally compares arrays by reference:
6
6
 
7
7
  ```ts
8
- [1, 2] === [1, 2]; // false
8
+ [1, "foo"] === [1, "foo"]; // false!
9
9
  ```
10
10
 
11
- `tuple-ts` gives tuples value-based identity through *interning*:
11
+ `tuple-ts` provides opt-in _value-based identity_:
12
12
 
13
13
  ```ts
14
14
  import { tuple } from "tuple-ts";
15
15
 
16
- tuple(1, 2) === tuple(1, 2); // true
16
+ tuple(1, "foo") === tuple(1, "foo"); // true
17
17
  ```
18
18
 
19
- Equivalent tuples created through the same tuple cache are canonicalised to the same object. Values are frozen, so their contents cannot change after interning. This makes tuples useful as composite `Map` keys, `Set` members, memoization keys, and immutable values in functional TypeScript.
19
+ with optional slot labels:
20
20
 
21
- `tuple-ts` also supports **named tuples** through `struct()`: values remain arrays with positional slots, while the declared labels are available as named properties.
21
+ ```ts
22
+ import { struct } from "tuple-ts";
22
23
 
23
- **Zero dependencies · Less than 1 KB gzipped**
24
+ const Person = struct(["name", "age"]);
24
25
 
25
- ## Install
26
+ const person1 = Person("Bob", 25);
26
27
 
27
- ```bash
28
- npm install tuple-ts
28
+ person1[0] === "Bob"; // true
29
+ person1.name === "Bob"; // true
29
30
  ```
30
31
 
31
- ## Tuples
32
-
33
- Create a tuple with narrow TypeScript inference:
32
+ and support for label-based construction:
34
33
 
35
34
  ```ts
36
- import { tuple } from "tuple-ts";
37
-
38
- const point = tuple(10, 20);
39
- // ^? readonly [10, 20]
35
+ const person2 = Person.from({ name: "Bob", age: 25 });
40
36
 
41
- point[0]; // 10
42
- point[1]; // 20
43
- Object.isFrozen(point); // true
37
+ person1 === person2; // true
44
38
  ```
45
39
 
46
- Calling `tuple()` again with the same values returns the same canonical object:
40
+ `tuple-ts` is useful when you want small immutable values with stable identity:
47
41
 
48
- ```ts
49
- const a = tuple(10, 20);
50
- const b = tuple(10, 20);
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
51
47
 
52
- a === b; // true
53
- Object.is(a, b); // true
54
- ```
48
+ `tuple-ts` has zero dependencies and is less than 1 KB gzipped.
55
49
 
56
- Different values or a different element order produce different tuples:
50
+ ## Install
57
51
 
58
- ```ts
59
- tuple(10, 20) === tuple(10, 21); // false
60
- tuple(10, 20) === tuple(20, 10); // false
52
+ ```bash
53
+ npm install tuple-ts
61
54
  ```
62
55
 
63
- Tuples are ordinary frozen arrays:
56
+ ## Tuples
64
57
 
65
- ```ts
66
- const t = tuple("hello", 42, true);
58
+ The values returned by `tuple(...)` are regular frozen JavaScript arrays:
67
59
 
68
- t.length; // 3
69
- t[0]; // "hello"
70
- [...t]; // ["hello", 42, true]
71
- Array.isArray(t); // true
72
- Object.isFrozen(t); // true
60
+ ```ts
61
+ const tuple1 = tuple(1, "foo");
62
+
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"]
73
69
  ```
74
70
 
75
- ### Constraining a tuple factory with `.narrow()`
76
-
77
- Use `.narrow()` to make a reusable factory whose arguments must satisfy a tuple type.
78
-
79
- Specify the type explicitly:
71
+ ...with automatically inferred literal types:
80
72
 
81
73
  ```ts
82
- const Point = tuple.narrow<[number, number]>();
83
-
84
- const a = Point(10, 20);
85
- const b = Point(10, 20);
86
-
87
- a === b; // true
88
- Point(10, "20"); // TypeScript error
74
+ type Tuple1 = typeof tuple1; // readonly [1, "foo"]
89
75
  ```
90
76
 
91
- You can also refine an existing fixed-length tuple factory using an exemplar. The exemplar is used to infer the constraint, not as a default value:
77
+ Type-narrowed tuple factories are also supported:
92
78
 
93
79
  ```ts
94
- const Pair = tuple.narrow<[unknown, unknown]>();
95
- const NumberPair = Pair.narrow([0, 0]);
80
+ type Point = [number, number];
81
+ const Point = tuple.typed<Point>();
82
+ // Or equivalently, using an exemplar:
83
+ // const Point = tuple.typed(0, 0);
96
84
 
97
- NumberPair(10, 20); // OK
98
- NumberPair(10, "20"); // TypeScript error
85
+ type P = Parameters<typeof Point>; // [number, number];
99
86
  ```
100
87
 
101
- In this example, the parent factory establishes the two-slot shape, and the exemplar narrows the element types to numbers. `.narrow()` is a TypeScript constraint; it does not create a new cache. Narrowed factories continue to use the shared tuple cache, so equal tuple values are interned together regardless of which narrowed factory constructs them.
102
-
103
- ## Why is this useful?
104
-
105
- ### Composite `Map` keys
106
-
107
- Without interning, separately constructed arrays don't work as value-based keys:
88
+ Calling `tuple()` again with the same values returns the same canonical object:
108
89
 
109
90
  ```ts
110
- const map = new Map();
91
+ const a = tuple(10, 20);
92
+ const b = tuple(10, 20);
111
93
 
112
- map.set([10, 20], "visited");
113
- map.get([10, 20]); // undefined
94
+ a === b; // true
95
+ Object.is(a, b); // true
114
96
  ```
115
97
 
116
- With `tuple-ts`:
98
+ Different values or a different element order produce different tuples:
117
99
 
118
100
  ```ts
119
- const map = new Map();
120
-
121
- map.set(tuple(10, 20), "visited");
122
- map.get(tuple(10, 20)); // "visited"
101
+ tuple(10, 20) === tuple(10, 21); // false!
102
+ tuple(10, 20) === tuple(20, 10); // false!
123
103
  ```
124
104
 
125
- The tuple used for lookup is the same canonical object as the tuple used as the key. This is useful for coordinates, memoization keys, graph nodes, dynamic-programming states, and other composite values.
105
+ ### Constraining a tuple factory with `.typed()`
126
106
 
127
- ### `Set` of values
107
+ Use `.typed()` to make a reusable tuple factory whose arguments must satisfy a nominated tuple type.
128
108
 
129
- ```ts
130
- const visited = new Set();
109
+ Specify the type explicitly:
131
110
 
132
- visited.add(tuple(10, 20));
111
+ ```ts
112
+ const Point = tuple.typed<[number, number]>();
133
113
 
134
- visited.has(tuple(10, 20)); // true
135
- visited.has(tuple(10, 21)); // false
114
+ Point(10, 20); // OK
115
+ Point(10, "20"); // TypeScript error
136
116
  ```
137
117
 
138
- No serialization, stringification, or custom hashing is required.
139
-
140
- ## Nested tuples
141
-
142
- Tuples can contain other interned tuples:
118
+ You can alternatively provide an exemplar from which the type constraint will be inferred:
143
119
 
144
120
  ```ts
145
- const a = tuple("matrix", tuple(1, 0), tuple(0, 1));
146
- const b = tuple("matrix", tuple(1, 0), tuple(0, 1));
121
+ const NumberPair = tuple.typed(0, 0);
147
122
 
148
- a === b; // true
123
+ NumberPair(10, 20); // OK
124
+ NumberPair(10, "20"); // TypeScript error
149
125
  ```
150
126
 
151
- Because the inner tuples are themselves canonicalised, the outer tuples can be interned as well.
127
+
128
+ ### Nesting tuples
129
+
130
+ Tuples can contain other interned tuples:
152
131
 
153
132
  ```ts
154
133
  const a = tuple("a", tuple("b", "c"));
@@ -156,17 +135,23 @@ const b = tuple("a", tuple("b", "c"));
156
135
  const c = tuple("a", tuple("b", "d"));
157
136
 
158
137
  a === b; // true
159
- a === c; // false
138
+ a === c; // false!
139
+
140
+ const mat1 = tuple("matrix", tuple(1, 0), tuple(0, 1));
141
+ const mat2 = tuple("matrix", tuple(1, 0), tuple(0, 1));
142
+
143
+ mat1 === mat2; // true
160
144
  ```
161
145
 
162
- ## Named tuples with `struct()`
146
+ ## Structs
163
147
 
164
- Use `struct()` when you want positional tuple storage with meaningful labels. Pass a readonly label list to create a reusable factory:
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.
165
150
 
166
151
  ```ts
167
152
  import { struct } from "tuple-ts";
168
153
 
169
- const Point = struct(["x", "y"]).narrow<[number, number]>();
154
+ const Point = struct(["x", "y"]);
170
155
 
171
156
  const p = Point(10, 20);
172
157
 
@@ -175,26 +160,24 @@ p[1]; // 20
175
160
  p.x; // 10
176
161
  p.y; // 20
177
162
 
178
- Array.isArray(p); // true
163
+ Array.isArray(p); // true!
179
164
  Object.isFrozen(p); // true
165
+ [ ...p ]; // [10, 20]
166
+ { ...p }; // { 0: 10, 1: 20, x: 10, y: 20 }
180
167
  ```
181
168
 
182
- The label order corresponds to the tuple slot order: the first label names slot `0`, the second names slot `1`, and so on. The named properties are added to the array when it is interned. They are own, enumerable properties and are frozen along with the array.
183
-
184
- ### Constructing from a named object with `.from()`
169
+ ### Constructing from an object with `.from()`
185
170
 
186
171
  `.from()` accepts an object with the declared labels and creates the corresponding positional tuple:
187
172
 
188
173
  ```ts
189
- const Point = struct(["x", "y"]).narrow<[number, number]>();
190
-
191
- const a = Point.from({ x: 10, y: 20 });
192
- const b = Point.from({ y: 20, x: 10 });
174
+ const Point = struct(["x", "y"]);
193
175
 
194
- a === b; // true
176
+ const point1 = Point.from({ x: 10, y: 20 });
195
177
 
196
- a[0]; // 10
197
- a.x; // 10
178
+ point1 === Point(10, 20); // true
179
+ point1[0] === 10; // true
180
+ point1.x === 10; // true
198
181
  ```
199
182
 
200
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:
@@ -210,45 +193,40 @@ The returned value contains the tuple slots and the schema's named properties, n
210
193
 
211
194
  ### Constraining a struct factory
212
195
 
213
- Use `.narrow()` to specify the tuple's positional types:
196
+ Just like `tuple`, structs support `.typed()` to specify the struct's value types positionally:
214
197
 
215
198
  ```ts
216
- const Point = struct(["x", "y"]).narrow<[number, number]>();
199
+ const Point = struct(["x", "y"]).typed<[number, number]>();
217
200
 
218
201
  Point(10, 20); // OK
219
202
  Point(10, "20"); // TypeScript error
220
203
  ```
221
204
 
222
- Use `.narrowFrom()` when you want to express the value types by label instead:
205
+ Use `.typedFrom()` when you want to express the value types by label instead:
223
206
 
224
207
  ```ts
225
208
  type Coordinates = { x: number; y: number };
226
209
 
227
- const Point = struct(["x", "y"]).narrowFrom<Coordinates>();
210
+ const Point = struct(["x", "y"]).typedFrom<Coordinates>();
228
211
 
229
212
  Point(10, 20); // OK
230
- Point(10, "20"); // TypeScript error
213
+ Point(10, "20"); // TypeScript error
231
214
  Point.from({ x: 10, y: 20 }); // OK
232
215
  ```
233
216
 
234
- You can also provide a typed exemplar to `.narrowFrom()`:
217
+ You can also provide a typed exemplar to `.typedFrom()`:
235
218
 
236
219
  ```ts
237
- const coordinateExample: Coordinates = { x: 0, y: 0 };
238
- const Point = struct(["x", "y"]).narrowFrom(coordinateExample);
220
+ const Point = struct(["x", "y"]).typedFrom({ x: 0, y: 0 });
239
221
  ```
240
222
 
241
- The exemplar establishes types; its values are not used as defaults. If you pass an inline `const` exemplar whose properties are literal values, those literal types may be preserved. Give the exemplar a widened type, or use an explicit generic argument, when you want types such as `number` rather than the literal type `0`.
242
-
243
- Both `.narrow()` and `.narrowFrom()` preserve the cache associated with the original `struct()` schema. They refine the factory's TypeScript constraints rather than creating a new interning domain.
244
-
245
223
  ### Struct schemas and identity
246
224
 
247
- Each call to `struct(labels)` creates a schema with its own intern cache. Factories narrowed from that schema share the cache, but separate calls to `struct()` create separate caches, even when given identical labels:
225
+ Each call to `struct(labels)` creates its own cache. Factories narrowed from that schema with `.typed()` share the cache, but separate calls to `struct()` create separate caches, even when given identical labels:
248
226
 
249
227
  ```ts
250
- const PointA = struct(["x", "y"]).narrow<[number, number]>();
251
- const PointB = struct(["x", "y"]).narrow<[number, number]>();
228
+ const PointA = struct(["x", "y"]).typed<[number, number]>();
229
+ const PointB = struct(["x", "y"]).typed<[number, number]>();
252
230
 
253
231
  PointA(1, 2) === PointA(1, 2); // true
254
232
  PointA(1, 2) === PointB(1, 2); // false
@@ -256,9 +234,40 @@ PointA(1, 2) === PointB(1, 2); // false
256
234
 
257
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.
258
236
 
259
- ### Choosing labels
237
+ ## Tuples and structs as composite keys for `Map` and `Set`
260
238
 
261
- Use distinct labels, and avoid labels that collide with array properties or indices. In particular, `"length"`, numeric-looking labels such as `"0"`, and method names such as `"map"` can conflict with normal array behaviour. The current implementation does not validate label names or duplicate labels, so callers should ensure labels are unique and suitable for named properties.
239
+ `tuple` and `struct` let you use composite keys with JavaScript's built-in `Map` and `Set`, without serialization or stringification.
240
+
241
+ JavaScript's `Map` and `Set` treat separately constructed arrays as distinct keys, even when their contents are equal:
242
+
243
+ ```ts
244
+ const myMap = new Map();
245
+ myMap.set([10, 20], "visited");
246
+
247
+ myMap.has([10, 20]); // false!
248
+ myMap.get([10, 20]); // undefined!
249
+
250
+ const mySet = new Set();
251
+ mySet.add([10, 20]);
252
+
253
+ mySet.has([10, 20]); // false!
254
+ ```
255
+
256
+ With `tuple-ts`:
257
+
258
+ ```ts
259
+ const myMap = new Map();
260
+ myMap.set(tuple(10, 20), "visited");
261
+
262
+ myMap.has(tuple(10, 20)); // true
263
+ myMap.get(tuple(10, 20)); // "visited"
264
+
265
+ const Point = struct(["x", "y"]);
266
+ const mySet = new Set();
267
+ mySet.add(Point(1, 2));
268
+
269
+ mySet.has(Point(1, 2)); // true
270
+ ```
262
271
 
263
272
  ## Equality semantics
264
273
 
@@ -278,7 +287,7 @@ tuple(value) === tuple(value); // true
278
287
  tuple({ x: 1 }) === tuple({ x: 1 }); // false
279
288
  ```
280
289
 
281
- `tuple-ts` does not recursively compare arbitrary JavaScript objects. Nested tuples work well because they are themselves interned:
290
+ `tuple-ts` does not recursively compare arbitrary JavaScript objects. Nested tuples work well because the values are themselves tuples:
282
291
 
283
292
  ```ts
284
293
  const a = tuple(tuple(1, 2));
@@ -287,45 +296,11 @@ const b = tuple(tuple(1, 2));
287
296
  a === b; // true
288
297
  ```
289
298
 
290
- Struct caches use the same shallow slot-value comparison, but each struct schema has its own cache. Within a schema, the label properties are determined by the schema; the indexed slot values determine which canonical tuple is returned.
291
-
292
- ## Immutable by design
293
-
294
- Tuples and structs returned by `tuple-ts` are frozen. Freezing is shallow, just like `Object.freeze()`:
295
-
296
- ```ts
297
- const details = { name: "Alice" };
298
- const value = tuple(details);
299
-
300
- details.name = "Bob"; // Allowed: the nested object itself is not frozen
301
- ```
302
-
303
- The tuple's reference to `details` is unchanged, so its interned identity remains the same. If you need immutable nested values, construct those values with `tuple()` or `struct()` too.
304
-
305
- ## Garbage-collection friendly
306
-
307
- Interning raises a natural question: *doesn't the cache grow forever?* The cache implementation uses weak references and finalization support so that values no longer referenced by the application can be reclaimed by the JavaScript garbage collector. Values are not intended to be kept alive forever simply because they were once constructed.
308
-
309
- ## When should I use it?
310
-
311
- `tuple-ts` is useful when you want small immutable values with stable identity:
312
-
313
- - Composite `Map` keys
314
- - `Set` membership
315
- - Memoization keys
316
- - Graph and search states
317
- - Coordinates and other multi-dimensional values
318
- - Functional programming
319
- - Immutable application state
320
- - Named, slot-based values such as points or pairs
321
-
322
- Use `tuple()` for positional values and `struct()` when the same kind of value benefits from stable field labels.
323
-
324
- ## Design
299
+ ## Garbage Collection
325
300
 
326
- The intern cache uses hashing to find candidates, then checks exact shallow equality before reusing an existing value. Hash collisions therefore do not determine equality. Tuple element order matters. Struct label order defines the mapping between slots and names, and each schema has its own cache.
301
+ `tuple-ts` works by maintaining internal caches of tuple/struct objects. This raises a natural question: *doesn't the cache grow forever?*
327
302
 
328
- 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.
329
304
 
330
305
  ## License
331
306