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 +143 -445
- package/dist/index.d.ts +8190 -44
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -19
- package/dist/intern-cache.d.ts +5 -4
- package/dist/intern-cache.d.ts.map +1 -1
- package/dist/intern-cache.js +13 -12
- package/package.json +7 -1
package/README.md
CHANGED
|
@@ -1,608 +1,306 @@
|
|
|
1
1
|
# tuple-ts
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**TypeScript-first tuples and structs with value-based identity.**
|
|
4
4
|
|
|
5
|
-
JavaScript normally compares arrays
|
|
5
|
+
JavaScript normally compares arrays by reference:
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
[1,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
+
with optional slot labels:
|
|
48
20
|
|
|
49
21
|
```ts
|
|
50
|
-
|
|
51
|
-
const b = tuple(10, 20);
|
|
22
|
+
import { struct } from "tuple-ts";
|
|
52
23
|
|
|
53
|
-
|
|
54
|
-
Object.is(a, b); // true
|
|
55
|
-
```
|
|
24
|
+
const Person = struct(["name", "age"]);
|
|
56
25
|
|
|
57
|
-
|
|
26
|
+
const person1 = Person("Bob", 25);
|
|
58
27
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
tuple(10, 20) === tuple(20, 10); // false
|
|
28
|
+
person1[0] === "Bob"; // true
|
|
29
|
+
person1.name === "Bob"; // true
|
|
62
30
|
```
|
|
63
31
|
|
|
64
|
-
|
|
32
|
+
and support for label-based construction:
|
|
65
33
|
|
|
66
34
|
```ts
|
|
67
|
-
const
|
|
35
|
+
const person2 = Person.from({ name: "Bob", age: 25 });
|
|
68
36
|
|
|
69
|
-
|
|
70
|
-
t[0]; // "hello"
|
|
71
|
-
[...t]; // ["hello", 42, true]
|
|
72
|
-
Object.isFrozen(t); // true
|
|
37
|
+
person1 === person2; // true
|
|
73
38
|
```
|
|
74
39
|
|
|
75
|
-
|
|
40
|
+
`tuple-ts` is useful when you want small immutable values with stable identity:
|
|
76
41
|
|
|
77
|
-
|
|
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
|
-
|
|
48
|
+
`tuple-ts` has zero dependencies and is less than 1 KB gzipped.
|
|
80
49
|
|
|
81
|
-
|
|
82
|
-
const map = new Map();
|
|
83
|
-
|
|
84
|
-
map.set([10, 20], "visited");
|
|
50
|
+
## Install
|
|
85
51
|
|
|
86
|
-
|
|
52
|
+
```bash
|
|
53
|
+
npm install tuple-ts
|
|
87
54
|
```
|
|
88
55
|
|
|
89
|
-
|
|
56
|
+
## Tuples
|
|
90
57
|
|
|
91
|
-
|
|
92
|
-
const map = new Map();
|
|
58
|
+
The values returned by `tuple(...)` are regular frozen JavaScript arrays:
|
|
93
59
|
|
|
94
|
-
|
|
60
|
+
```ts
|
|
61
|
+
const tuple1 = tuple(1, "foo");
|
|
95
62
|
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
## Nested tuples
|
|
119
|
-
|
|
120
|
-
Tuples can be nested:
|
|
77
|
+
Type-narrowed tuple factories are also supported:
|
|
121
78
|
|
|
122
79
|
```ts
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
85
|
+
type P = Parameters<typeof Point>; // [number, number];
|
|
136
86
|
```
|
|
137
87
|
|
|
138
|
-
|
|
88
|
+
Calling `tuple()` again with the same values returns the same canonical object:
|
|
139
89
|
|
|
140
90
|
```ts
|
|
141
|
-
const a = tuple(
|
|
142
|
-
const b = tuple(
|
|
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
|
|
95
|
+
Object.is(a, b); // true
|
|
147
96
|
```
|
|
148
97
|
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
105
|
+
### Constraining a tuple factory with `.narrow()`
|
|
165
106
|
|
|
166
|
-
|
|
107
|
+
Use `.narrow()` to make a reusable tuple factory whose arguments must satisfy a nominated tuple type.
|
|
167
108
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Alternatively, specify the type directly:
|
|
109
|
+
Specify the type explicitly:
|
|
171
110
|
|
|
172
111
|
```ts
|
|
173
|
-
|
|
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
|
-
|
|
114
|
+
Point(10, 20); // OK
|
|
115
|
+
Point(10, "20"); // TypeScript error
|
|
181
116
|
```
|
|
182
117
|
|
|
183
|
-
|
|
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
|
|
121
|
+
const NumberPair = tuple.narrow(0, 0);
|
|
191
122
|
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
128
|
+
### Nesting tuples
|
|
199
129
|
|
|
200
|
-
|
|
130
|
+
Tuples can contain other interned tuples:
|
|
201
131
|
|
|
202
132
|
```ts
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
223
|
-
Object.isFrozen(a); // true
|
|
143
|
+
mat1 === mat2; // true
|
|
224
144
|
```
|
|
225
145
|
|
|
226
|
-
|
|
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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
151
|
+
```ts
|
|
152
|
+
import { struct } from "tuple-ts";
|
|
242
153
|
|
|
243
|
-
|
|
154
|
+
const Point = struct(["x", "y"]);
|
|
244
155
|
|
|
245
|
-
|
|
156
|
+
const p = Point(10, 20);
|
|
246
157
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
158
|
+
p[0]; // 10
|
|
159
|
+
p[1]; // 20
|
|
160
|
+
p.x; // 10
|
|
161
|
+
p.y; // 20
|
|
250
162
|
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
169
|
+
### Constructing from an object with `.from()`
|
|
256
170
|
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
const point1 = Point.from({ x: 10, y: 20 });
|
|
288
177
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
|
186
|
+
const p = Point.from({ x: 10, y: 20, altitude: 100 });
|
|
305
187
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
a === b; // false
|
|
188
|
+
p.x; // 10
|
|
189
|
+
p.y; // 20
|
|
310
190
|
```
|
|
311
191
|
|
|
312
|
-
|
|
192
|
+
The returned value contains the tuple slots and the schema's named properties, not the unrelated input properties.
|
|
313
193
|
|
|
314
|
-
|
|
194
|
+
### Constraining a struct factory
|
|
315
195
|
|
|
316
|
-
|
|
196
|
+
Just like `tuple`, structs support `.narrow()` to specify the struct's value types positionally:
|
|
317
197
|
|
|
318
198
|
```ts
|
|
319
|
-
|
|
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
|
-
|
|
201
|
+
Point(10, 20); // OK
|
|
202
|
+
Point(10, "20"); // TypeScript error
|
|
327
203
|
```
|
|
328
204
|
|
|
329
|
-
|
|
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
|
-
|
|
208
|
+
type Coordinates = { x: number; y: number };
|
|
335
209
|
|
|
336
|
-
const
|
|
337
|
-
const b = Point({ x: 1, y: 2, z: 3 });
|
|
210
|
+
const Point = struct(["x", "y"]).narrowFrom<Coordinates>();
|
|
338
211
|
|
|
339
|
-
|
|
212
|
+
Point(10, 20); // OK
|
|
213
|
+
Point(10, "20"); // TypeScript error
|
|
214
|
+
Point.from({ x: 10, y: 20 }); // OK
|
|
340
215
|
```
|
|
341
216
|
|
|
342
|
-
|
|
217
|
+
You can also provide a typed exemplar to `.narrowFrom()`:
|
|
343
218
|
|
|
344
219
|
```ts
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
b.z; // TypeScript error
|
|
220
|
+
const Point = struct(["x", "y"]).narrowFrom({ x: 0, y: 0 });
|
|
348
221
|
```
|
|
349
222
|
|
|
350
|
-
|
|
223
|
+
### Struct schemas and identity
|
|
351
224
|
|
|
352
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
363
|
-
|
|
231
|
+
PointA(1, 2) === PointA(1, 2); // true
|
|
232
|
+
PointA(1, 2) === PointB(1, 2); // false
|
|
364
233
|
```
|
|
365
234
|
|
|
366
|
-
|
|
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
|
-
|
|
237
|
+
## Tuples and structs as composite keys for `Map` and `Set`
|
|
369
238
|
|
|
370
|
-
|
|
239
|
+
`tuple` and `struct` let you use composite keys with JavaScript's built-in `Map` and `Set`, without serialization or stringification.
|
|
371
240
|
|
|
372
|
-
|
|
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
|
|
376
|
-
|
|
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
|
-
|
|
247
|
+
myMap.has([10, 20]); // false!
|
|
248
|
+
myMap.get([10, 20]); // undefined!
|
|
385
249
|
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
253
|
+
mySet.has([10, 20]); // false!
|
|
395
254
|
```
|
|
396
255
|
|
|
397
|
-
|
|
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
|
|
259
|
+
const myMap = new Map();
|
|
260
|
+
myMap.set(tuple(10, 20), "visited");
|
|
407
261
|
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
461
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|