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 +257 -88
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/struct.spec.js +7 -0
- package/dist/tuple.spec.js +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,161 +1,330 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tuple-ts
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Immutable tuples and structs with value-based identity for JavaScript and TypeScript.**
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
```ts
|
|
48
|
+
const a = tuple(10, 20);
|
|
49
|
+
const b = tuple(10, 20);
|
|
24
50
|
|
|
25
|
-
|
|
51
|
+
a === b; // true
|
|
52
|
+
Object.is(a, b); // true
|
|
53
|
+
```
|
|
26
54
|
|
|
27
|
-
|
|
55
|
+
Different values give you different objects:
|
|
28
56
|
|
|
29
57
|
```ts
|
|
30
|
-
|
|
58
|
+
tuple(10, 20) === tuple(10, 21); // false
|
|
59
|
+
tuple(10, 20) === tuple(20, 10); // false
|
|
60
|
+
```
|
|
31
61
|
|
|
32
|
-
|
|
33
|
-
const b = tuple(1, "hello", true);
|
|
62
|
+
Tuples are ordinary frozen arrays, so there is no special API to learn:
|
|
34
63
|
|
|
35
|
-
|
|
36
|
-
|
|
64
|
+
```ts
|
|
65
|
+
const t = tuple("hello", 42, true);
|
|
37
66
|
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
73
|
+
## Why is this useful?
|
|
46
74
|
|
|
47
|
-
|
|
75
|
+
### Composite `Map` keys
|
|
76
|
+
|
|
77
|
+
JavaScript doesn't have value-based arrays, so this doesn't work:
|
|
48
78
|
|
|
49
79
|
```ts
|
|
50
|
-
|
|
80
|
+
const map = new Map();
|
|
51
81
|
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
console.log(t1 === t3); // false
|
|
84
|
+
map.get([10, 20]); // undefined
|
|
58
85
|
```
|
|
59
86
|
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
+
type Point = [x: number, y: number];
|
|
89
153
|
|
|
90
|
-
|
|
91
|
-
const Point2D = tuple<Point2D>;
|
|
154
|
+
const Point = tuple<Point>;
|
|
92
155
|
|
|
93
|
-
const
|
|
94
|
-
const
|
|
156
|
+
const a = Point(10, 20);
|
|
157
|
+
const b = Point(10, 20);
|
|
95
158
|
|
|
96
|
-
|
|
97
|
-
console.log(Object.is(p1, p2)); // true
|
|
159
|
+
a === b; // true
|
|
98
160
|
```
|
|
99
161
|
|
|
100
|
-
|
|
162
|
+
The result retains the tuple's TypeScript type while gaining the same runtime identity semantics.
|
|
101
163
|
|
|
102
|
-
Structs
|
|
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
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
184
|
+
Property order does not matter.
|
|
117
185
|
|
|
118
|
-
|
|
186
|
+
Structs are frozen:
|
|
119
187
|
|
|
120
188
|
```ts
|
|
121
|
-
|
|
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
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
console.log(user1.name); // "Alice"
|
|
232
|
+
a === b; // true
|
|
135
233
|
```
|
|
136
234
|
|
|
137
|
-
|
|
235
|
+
## Equality semantics
|
|
138
236
|
|
|
139
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
##
|
|
328
|
+
## License
|
|
152
329
|
|
|
153
|
-
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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));
|
package/dist/struct.spec.js
CHANGED
|
@@ -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
|
});
|
package/dist/tuple.spec.js
CHANGED
|
@@ -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
|
+
});
|