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 +133 -158
- package/dist/index.d.ts +8200 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -1,154 +1,133 @@
|
|
|
1
1
|
# tuple-ts
|
|
2
2
|
|
|
3
|
-
**
|
|
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,
|
|
8
|
+
[1, "foo"] === [1, "foo"]; // false!
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`tuple-ts`
|
|
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,
|
|
16
|
+
tuple(1, "foo") === tuple(1, "foo"); // true
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
with optional slot labels:
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
```ts
|
|
22
|
+
import { struct } from "tuple-ts";
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
const Person = struct(["name", "age"]);
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
const person1 = Person("Bob", 25);
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
person1[0] === "Bob"; // true
|
|
29
|
+
person1.name === "Bob"; // true
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Create a tuple with narrow TypeScript inference:
|
|
32
|
+
and support for label-based construction:
|
|
34
33
|
|
|
35
34
|
```ts
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const point = tuple(10, 20);
|
|
39
|
-
// ^? readonly [10, 20]
|
|
35
|
+
const person2 = Person.from({ name: "Bob", age: 25 });
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
point[1]; // 20
|
|
43
|
-
Object.isFrozen(point); // true
|
|
37
|
+
person1 === person2; // true
|
|
44
38
|
```
|
|
45
39
|
|
|
46
|
-
|
|
40
|
+
`tuple-ts` is useful when you want small immutable values with stable identity:
|
|
47
41
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
Object.is(a, b); // true
|
|
54
|
-
```
|
|
48
|
+
`tuple-ts` has zero dependencies and is less than 1 KB gzipped.
|
|
55
49
|
|
|
56
|
-
|
|
50
|
+
## Install
|
|
57
51
|
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
tuple(10, 20) === tuple(20, 10); // false
|
|
52
|
+
```bash
|
|
53
|
+
npm install tuple-ts
|
|
61
54
|
```
|
|
62
55
|
|
|
63
|
-
Tuples
|
|
56
|
+
## Tuples
|
|
64
57
|
|
|
65
|
-
|
|
66
|
-
const t = tuple("hello", 42, true);
|
|
58
|
+
The values returned by `tuple(...)` are regular frozen JavaScript arrays:
|
|
67
59
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
Array.isArray(
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
Type-narrowed tuple factories are also supported:
|
|
92
78
|
|
|
93
79
|
```ts
|
|
94
|
-
|
|
95
|
-
const
|
|
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
|
-
|
|
98
|
-
NumberPair(10, "20"); // TypeScript error
|
|
85
|
+
type P = Parameters<typeof Point>; // [number, number];
|
|
99
86
|
```
|
|
100
87
|
|
|
101
|
-
|
|
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
|
|
91
|
+
const a = tuple(10, 20);
|
|
92
|
+
const b = tuple(10, 20);
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
|
|
94
|
+
a === b; // true
|
|
95
|
+
Object.is(a, b); // true
|
|
114
96
|
```
|
|
115
97
|
|
|
116
|
-
|
|
98
|
+
Different values or a different element order produce different tuples:
|
|
117
99
|
|
|
118
100
|
```ts
|
|
119
|
-
|
|
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
|
-
|
|
105
|
+
### Constraining a tuple factory with `.typed()`
|
|
126
106
|
|
|
127
|
-
|
|
107
|
+
Use `.typed()` to make a reusable tuple factory whose arguments must satisfy a nominated tuple type.
|
|
128
108
|
|
|
129
|
-
|
|
130
|
-
const visited = new Set();
|
|
109
|
+
Specify the type explicitly:
|
|
131
110
|
|
|
132
|
-
|
|
111
|
+
```ts
|
|
112
|
+
const Point = tuple.typed<[number, number]>();
|
|
133
113
|
|
|
134
|
-
|
|
135
|
-
|
|
114
|
+
Point(10, 20); // OK
|
|
115
|
+
Point(10, "20"); // TypeScript error
|
|
136
116
|
```
|
|
137
117
|
|
|
138
|
-
|
|
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
|
|
146
|
-
const b = tuple("matrix", tuple(1, 0), tuple(0, 1));
|
|
121
|
+
const NumberPair = tuple.typed(0, 0);
|
|
147
122
|
|
|
148
|
-
|
|
123
|
+
NumberPair(10, 20); // OK
|
|
124
|
+
NumberPair(10, "20"); // TypeScript error
|
|
149
125
|
```
|
|
150
126
|
|
|
151
|
-
|
|
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
|
-
##
|
|
146
|
+
## Structs
|
|
163
147
|
|
|
164
|
-
Use `struct()` when you want
|
|
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"])
|
|
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
|
-
|
|
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"])
|
|
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
|
-
|
|
176
|
+
const point1 = Point.from({ x: 10, y: 20 });
|
|
195
177
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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"]).
|
|
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 `.
|
|
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"]).
|
|
210
|
+
const Point = struct(["x", "y"]).typedFrom<Coordinates>();
|
|
228
211
|
|
|
229
212
|
Point(10, 20); // OK
|
|
230
|
-
Point(10, "20");
|
|
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 `.
|
|
217
|
+
You can also provide a typed exemplar to `.typedFrom()`:
|
|
235
218
|
|
|
236
219
|
```ts
|
|
237
|
-
const
|
|
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
|
|
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"]).
|
|
251
|
-
const PointB = struct(["x", "y"]).
|
|
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
|
-
|
|
237
|
+
## Tuples and structs as composite keys for `Map` and `Set`
|
|
260
238
|
|
|
261
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|