tuple-ts 0.5.0 → 0.7.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,26 +1,24 @@
1
1
  # tuple-ts
2
2
 
3
- **Immutable tuples and structs with value-based identity for JavaScript and TypeScript.**
3
+ **Immutable interned tuples and named tuples for JavaScript and TypeScript.**
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
8
+ [1, 2] === [1, 2]; // false
11
9
  ```
12
10
 
13
- `tuple-ts` gives you a different model:
11
+ `tuple-ts` gives tuples value-based identity through *interning*:
14
12
 
15
13
  ```ts
16
- tuple(1, 2) === tuple(1, 2) // true
14
+ import { tuple } from "tuple-ts";
17
15
 
18
- record({ x: 1 }) === record({ x: 1 }) // true
16
+ tuple(1, 2) === tuple(1, 2); // true
19
17
  ```
20
18
 
21
- Equal values are **interned to the same object**.
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.
22
20
 
23
- That makes tuples and structs immutable, naturally usable as composite `Map` keys and `Set` values, and particularly pleasant to use in functional TypeScript.
21
+ `tuple-ts` also supports **named tuples** through `struct()`: values remain arrays with positional slots, while the declared labels are available as named properties.
24
22
 
25
23
  **Zero dependencies · Less than 1 KB gzipped**
26
24
 
@@ -32,7 +30,7 @@ npm install tuple-ts
32
30
 
33
31
  ## Tuples
34
32
 
35
- Create an immutable tuple with full TypeScript inference:
33
+ Create a tuple with narrow TypeScript inference:
36
34
 
37
35
  ```ts
38
36
  import { tuple } from "tuple-ts";
@@ -40,11 +38,12 @@ import { tuple } from "tuple-ts";
40
38
  const point = tuple(10, 20);
41
39
  // ^? readonly [10, 20]
42
40
 
43
- console.log(point[0]); // 10
44
- console.log(point[1]); // 20
41
+ point[0]; // 10
42
+ point[1]; // 20
43
+ Object.isFrozen(point); // true
45
44
  ```
46
45
 
47
- Calling `tuple` again with the same values gives you the **same object**:
46
+ Calling `tuple()` again with the same values returns the same canonical object:
48
47
 
49
48
  ```ts
50
49
  const a = tuple(10, 20);
@@ -54,14 +53,14 @@ a === b; // true
54
53
  Object.is(a, b); // true
55
54
  ```
56
55
 
57
- Different values give you different objects:
56
+ Different values or a different element order produce different tuples:
58
57
 
59
58
  ```ts
60
59
  tuple(10, 20) === tuple(10, 21); // false
61
60
  tuple(10, 20) === tuple(20, 10); // false
62
61
  ```
63
62
 
64
- Tuples are ordinary frozen arrays, so there is no special API to learn:
63
+ Tuples are ordinary frozen arrays:
65
64
 
66
65
  ```ts
67
66
  const t = tuple("hello", 42, true);
@@ -69,20 +68,48 @@ const t = tuple("hello", 42, true);
69
68
  t.length; // 3
70
69
  t[0]; // "hello"
71
70
  [...t]; // ["hello", 42, true]
71
+ Array.isArray(t); // true
72
72
  Object.isFrozen(t); // true
73
73
  ```
74
74
 
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:
80
+
81
+ ```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
89
+ ```
90
+
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:
92
+
93
+ ```ts
94
+ const Pair = tuple.narrow<[unknown, unknown]>();
95
+ const NumberPair = Pair.narrow([0, 0]);
96
+
97
+ NumberPair(10, 20); // OK
98
+ NumberPair(10, "20"); // TypeScript error
99
+ ```
100
+
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
+
75
103
  ## Why is this useful?
76
104
 
77
105
  ### Composite `Map` keys
78
106
 
79
- JavaScript doesn't have value-based arrays, so this doesn't work:
107
+ Without interning, separately constructed arrays don't work as value-based keys:
80
108
 
81
109
  ```ts
82
110
  const map = new Map();
83
111
 
84
112
  map.set([10, 20], "visited");
85
-
86
113
  map.get([10, 20]); // undefined
87
114
  ```
88
115
 
@@ -92,18 +119,13 @@ With `tuple-ts`:
92
119
  const map = new Map();
93
120
 
94
121
  map.set(tuple(10, 20), "visited");
95
-
96
122
  map.get(tuple(10, 20)); // "visited"
97
123
  ```
98
124
 
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.
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.
102
126
 
103
127
  ### `Set` of values
104
128
 
105
- The same applies to `Set`:
106
-
107
129
  ```ts
108
130
  const visited = new Set();
109
131
 
@@ -117,25 +139,16 @@ No serialization, stringification, or custom hashing is required.
117
139
 
118
140
  ## Nested tuples
119
141
 
120
- Tuples can be nested:
142
+ Tuples can contain other interned tuples:
121
143
 
122
144
  ```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
- );
145
+ const a = tuple("matrix", tuple(1, 0), tuple(0, 1));
146
+ const b = tuple("matrix", tuple(1, 0), tuple(0, 1));
134
147
 
135
148
  a === b; // true
136
149
  ```
137
150
 
138
- Because nested tuples themselves have value-based identity, the resulting structures are canonicalised naturally.
151
+ Because the inner tuples are themselves canonicalised, the outer tuples can be interned as well.
139
152
 
140
153
  ```ts
141
154
  const a = tuple("a", tuple("b", "c"));
@@ -146,215 +159,174 @@ a === b; // true
146
159
  a === c; // false
147
160
  ```
148
161
 
149
- ## Typed tuples
162
+ ## Named tuples with `struct()`
150
163
 
151
- You can create a reusable constructor for a particular tuple type:
164
+ Use `struct()` when you want positional tuple storage with meaningful labels. Pass a readonly label list to create a reusable factory:
152
165
 
153
166
  ```ts
154
- type Point = [x: number, y: number];
167
+ import { struct } from "tuple-ts";
155
168
 
156
- const Point = tuple<Point>;
169
+ const Point = struct(["x", "y"]).narrow<[number, number]>();
157
170
 
158
- const a = Point(10, 20);
159
- const b = Point(10, 20);
171
+ const p = Point(10, 20);
160
172
 
161
- a === b; // true
173
+ p[0]; // 10
174
+ p[1]; // 20
175
+ p.x; // 10
176
+ p.y; // 20
177
+
178
+ Array.isArray(p); // true
179
+ Object.isFrozen(p); // true
162
180
  ```
163
181
 
164
- The result retains the tuple's TypeScript type while gaining the same runtime identity semantics.
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.
165
183
 
166
- ## Records
184
+ ### Constructing from a named object with `.from()`
167
185
 
168
- `record` provides the same value-based identity semantics for objects:
186
+ `.from()` accepts an object with the declared labels and creates the corresponding positional tuple:
169
187
 
170
188
  ```ts
171
- import { record } from "tuple-ts";
172
-
173
- const a = record({
174
- id: 1,
175
- role: "admin",
176
- });
189
+ const Point = struct(["x", "y"]).narrow<[number, number]>();
177
190
 
178
- const b = record({
179
- role: "admin",
180
- id: 1,
181
- });
191
+ const a = Point.from({ x: 10, y: 20 });
192
+ const b = Point.from({ y: 20, x: 10 });
182
193
 
183
194
  a === b; // true
184
- ```
185
-
186
- Property order does not matter.
187
195
 
188
- Records are frozen:
189
-
190
- ```ts
191
- Object.isFrozen(a); // true
196
+ a[0]; // 10
197
+ a.x; // 10
192
198
  ```
193
199
 
194
- And they work naturally as `Map` keys:
200
+ 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:
195
201
 
196
202
  ```ts
197
- const users = new Map();
203
+ const p = Point.from({ x: 10, y: 20, altitude: 100 });
198
204
 
199
- users.set(
200
- record({ id: 42, role: "admin" }),
201
- "Alice",
202
- );
203
-
204
- users.get(
205
- record({ role: "admin", id: 42 }),
206
- ); // "Alice"
205
+ p.x; // 10
206
+ p.y; // 20
207
207
  ```
208
208
 
209
- ### Typed records
209
+ The returned value contains the tuple slots and the schema's named properties, not the unrelated input properties.
210
+
211
+ ### Constraining a struct factory
210
212
 
211
- As with tuples, you can define a reusable typed constructor by passing a type argument:
213
+ Use `.narrow()` to specify the tuple's positional types:
212
214
 
213
215
  ```ts
214
- interface User {
215
- id: string;
216
- name: string;
217
- updatedAt: number;
218
- }
219
-
220
- const User = record<User>;
221
-
222
- const a = User({
223
- id: "alice",
224
- name: "Alice",
225
- updatedAt: 1000,
226
- });
227
-
228
- const b = User({
229
- name: "Alice",
230
- id: "alice",
231
- updatedAt: 1000,
232
- });
216
+ const Point = struct(["x", "y"]).narrow<[number, number]>();
233
217
 
234
- a === b; // true
218
+ Point(10, 20); // OK
219
+ Point(10, "20"); // TypeScript error
235
220
  ```
236
221
 
237
- ## Structs
222
+ Use `.narrowFrom()` when you want to express the value types by label instead:
223
+
224
+ ```ts
225
+ type Coordinates = { x: number; y: number };
238
226
 
239
- `record` interns whatever shape you hand it — which means any difference in keys or values produces a different object. If you want something more locked-down, where a fixed declared set of keys defines identity, use `struct`.
227
+ const Point = struct(["x", "y"]).narrowFrom<Coordinates>();
228
+
229
+ Point(10, 20); // OK
230
+ Point(10, "20"); // TypeScript error
231
+ Point.from({ x: 10, y: 20 }); // OK
232
+ ```
240
233
 
241
- `struct` takes key names as arguments and returns a factory. The factory accepts any object that has at least those keys, extracts exactly those keys, and interns the result.
234
+ You can also provide a typed exemplar to `.narrowFrom()`:
242
235
 
243
236
  ```ts
244
- import { struct } from "tuple-ts";
237
+ const coordinateExample: Coordinates = { x: 0, y: 0 };
238
+ const Point = struct(["x", "y"]).narrowFrom(coordinateExample);
239
+ ```
245
240
 
246
- const Point = struct("x", "y");
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`.
247
242
 
248
- const a = Point({ x: 1, y: 2 });
249
- const b = Point({ x: 1, y: 2 });
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.
250
244
 
251
- a === b; // true
252
- ```
245
+ ### Struct schemas and identity
253
246
 
254
- Because the schema is fixed at construction time, equality is always based on precisely those keys — no more, no less:
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:
255
248
 
256
249
  ```ts
257
- const a = Point({ x: 1, y: 2 });
258
- const b = Point({ x: 1, y: 3 });
250
+ const PointA = struct(["x", "y"]).narrow<[number, number]>();
251
+ const PointB = struct(["x", "y"]).narrow<[number, number]>();
259
252
 
260
- a === b; // false — y differs
253
+ PointA(1, 2) === PointA(1, 2); // true
254
+ PointA(1, 2) === PointB(1, 2); // false
261
255
  ```
262
256
 
263
- All keys passed to `struct` must be distinct strings. Passing duplicate keys throws a `DuplicateKeyError`.
257
+ 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
+
259
+ ### Choosing labels
260
+
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.
264
262
 
265
263
  ## Equality semantics
266
264
 
267
- Primitive values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
265
+ Tuple elements are compared shallowly using `Object.is`:
268
266
 
269
267
  ```ts
270
268
  tuple(NaN) === tuple(NaN); // true
271
-
272
269
  tuple(+0) === tuple(-0); // false
273
270
  ```
274
271
 
275
- Object values retain JavaScript's normal identity semantics.
276
-
277
- The same object is the same value:
272
+ Object-valued elements retain JavaScript's normal reference identity:
278
273
 
279
274
  ```ts
280
275
  const value = { x: 1 };
281
-
282
276
  tuple(value) === tuple(value); // true
283
- ```
284
-
285
- Two separately created objects are different values:
286
277
 
287
- ```ts
288
278
  tuple({ x: 1 }) === tuple({ x: 1 }); // false
289
279
  ```
290
280
 
291
- This is **shallow equality**. `tuple-ts` does not recursively compare arbitrary JavaScript objects.
292
-
293
- Nested tuples and structs work particularly well because they are themselves canonical values.
294
-
295
- ## Immutable by design
296
-
297
- Tuples and structs returned by `tuple-ts` are frozen:
281
+ `tuple-ts` does not recursively compare arbitrary JavaScript objects. Nested tuples work well because they are themselves interned:
298
282
 
299
283
  ```ts
300
- const point = tuple(10, 20);
284
+ const a = tuple(tuple(1, 2));
285
+ const b = tuple(tuple(1, 2));
301
286
 
302
- Object.isFrozen(point); // true
287
+ a === b; // true
303
288
  ```
304
289
 
305
- 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.
306
-
307
- ## Garbage-collection friendly
308
-
309
- Interning values creates a natural question: *doesn't the cache grow forever?*
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.
310
291
 
311
- `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.
312
-
313
- In other words, you don't have to manually maintain a global cache of every tuple you've ever created.
314
-
315
- ## TypeScript first
292
+ ## Immutable by design
316
293
 
317
- `tuple`, `record`, and `struct` are designed to preserve TypeScript's inference:
294
+ Tuples and structs returned by `tuple-ts` are frozen. Freezing is shallow, just like `Object.freeze()`:
318
295
 
319
296
  ```ts
320
- const value = tuple(
321
- "hello",
322
- 42,
323
- true,
324
- );
297
+ const details = { name: "Alice" };
298
+ const value = tuple(details);
325
299
 
326
- // readonly ["hello", 42, true]
300
+ details.name = "Bob"; // Allowed: the nested object itself is not frozen
327
301
  ```
328
302
 
329
- The library is deliberately small: the runtime API consists of three constructors:
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.
330
304
 
331
- ```ts
332
- import { tuple, record, struct } from "tuple-ts";
333
- ```
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.
334
308
 
335
309
  ## When should I use it?
336
310
 
337
- `tuple-ts` is particularly useful when you want **small immutable values with stable identity**:
311
+ `tuple-ts` is useful when you want small immutable values with stable identity:
338
312
 
339
- * composite `Map` keys
340
- * `Set` membership
341
- * memoization keys
342
- * graph/search states
343
- * coordinates and other multi-dimensional values
344
- * functional programming
345
- * immutable application state
346
- * canonical representations of frequently repeated values
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
347
321
 
348
- 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**.
322
+ Use `tuple()` for positional values and `struct()` when the same kind of value benefits from stable field labels.
349
323
 
350
324
  ## Design
351
325
 
352
- 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.
353
-
354
- The intern cache uses weak references, so canonical values do not need to be kept alive solely because they have previously been created.
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.
355
327
 
356
328
  There are no runtime dependencies.
357
329
 
358
330
  ## License
359
331
 
360
- ISC
332
+ ISC
package/dist/index.d.ts CHANGED
@@ -1,11 +1,44 @@
1
1
  import InternCache from "./intern-cache";
2
- export declare const _tupleCache: InternCache<number, readonly unknown[]>;
3
- export declare const _recordCache: InternCache<number, Record<string, unknown>>;
4
- export declare const tuple: <const T extends readonly unknown[]>(...t: T) => T;
5
- export declare const record: <const T extends Record<string, unknown>>({ ...r }: T) => T;
6
- export declare class DuplicateKeyError extends Error {
7
- readonly name = "DuplicateKeyError";
8
- constructor();
9
- }
10
- export declare const struct: <const K extends Array<string>>(...keys: K) => <const T extends Record<K[number], unknown>>(s: T) => Pick<T, K[number]>;
2
+ type BaseTuple = readonly unknown[];
3
+ type BaseLabels = readonly string[];
4
+ type ObjectOf<T extends BaseTuple, L extends BaseLabels> = {
5
+ [I in Extract<keyof L, `${number}`> as L[I]]: T[I extends `${infer N extends number}` ? N : never];
6
+ };
7
+ type StructOf<T extends BaseTuple, L extends BaseLabels> = Readonly<T & ObjectOf<T, L>>;
8
+ type TupleFromObject<T extends BaseTuple, L extends BaseLabels, O extends ObjectOf<T, L>> = T & {
9
+ [I in keyof L]: O[L[I]];
10
+ };
11
+ type StructFactory<T extends BaseTuple, L extends BaseLabels> = (<const T_ extends T>(...args: T_) => StructOf<T_, L>) & {
12
+ from: <const O extends ObjectOf<T, L>>(arg: O) => Readonly<O & TupleFromObject<T, L, O>>;
13
+ narrow: <const T_ extends T>() => StructFactory<T_, L>;
14
+ narrowFrom: <const O extends ObjectOf<T, L>>(..._args: [exemplar: O] | []) => StructFactory<{
15
+ [I in keyof L]: O[L[I]];
16
+ }, L>;
17
+ };
18
+ export declare const _tupleCache: InternCache<number, readonly unknown[], readonly unknown[]>;
19
+ export declare const tuple: (<const T_ extends BaseTuple>(...args: T_) => T_) & {
20
+ narrow: <T_ extends BaseTuple>(..._args: [] | [exemplar: T_]) => (<const T__1 extends T_>(...args: T__1) => T__1) & {
21
+ narrow: <T__1 extends T_>(..._args: [] | [exemplar: T__1]) => (<const T__2 extends T__1>(...args: T__2) => T__2) & {
22
+ narrow: <T__2 extends T__1>(..._args: [] | [exemplar: T__2]) => (<const T__3 extends T__2>(...args: T__3) => T__3) & {
23
+ narrow: <T__3 extends T__2>(..._args: [] | [exemplar: T__3]) => (<const T__4 extends T__3>(...args: T__4) => T__4) & {
24
+ narrow: <T__4 extends T__3>(..._args: [] | [exemplar: T__4]) => (<const T__5 extends T__4>(...args: T__5) => T__5) & {
25
+ narrow: <T__5 extends T__4>(..._args: [] | [exemplar: T__5]) => (<const T__6 extends T__5>(...args: T__6) => T__6) & {
26
+ narrow: <T__6 extends T__5>(..._args: [] | [exemplar: T__6]) => (<const T__7 extends T__6>(...args: T__7) => T__7) & {
27
+ narrow: <T__7 extends T__6>(..._args: [] | [exemplar: T__7]) => (<const T__8 extends T__7>(...args: T__8) => T__8) & {
28
+ narrow: <T__8 extends T__7>(..._args: [] | [exemplar: T__8]) => (<const T__9 extends T__8>(...args: T__9) => T__9) & {
29
+ narrow: <T__9 extends T__8>(..._args: [] | [exemplar: T__9]) => (<const T__10 extends T__9>(...args: T__10) => T__10) & {
30
+ narrow: <T__10 extends T__9>(..._args: [] | [exemplar: T__10]) => (<const T__11 extends T__10>(...args: T__11) => T__11) & /*elided*/ any;
31
+ };
32
+ };
33
+ };
34
+ };
35
+ };
36
+ };
37
+ };
38
+ };
39
+ };
40
+ };
41
+ };
42
+ export declare const struct: <const L extends readonly string[]>(labels: L) => StructFactory<BaseTuple, L>;
43
+ export {};
11
44
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AA6BzC,eAAO,MAAM,WAAW,yCAIvB,CAAC;AAEF,eAAO,MAAM,YAAY,8CASxB,CAAC;AAEF,eAAO,MAAM,KAAK,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,MAC7C,CAAC;AAErB,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,MACtD,CAAC;AAEtB,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,SAAkB,IAAI,uBAAuB;;CAI9C;AAED,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,KAAK,CAAC,MAAM,CAAC,EAAE,GAAG,MAAM,CAAC,MAetD,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,EAAE,GAAG,CAAC,uBAOzD,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,gBAAgB,CAAC;AAGzC,KAAK,SAAS,GAAG,SAAS,OAAO,EAAE,CAAC;AACpC,KAAK,UAAU,GAAG,SAAS,MAAM,EAAE,CAAC;AAqBpC,KAAK,QAAQ,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,IAAI;KACxD,CAAC,IAAI,OAAO,CACX,MAAM,CAAC,EACP,GAAG,MAAM,EAAE,CACZ,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,MAAM,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC;CACjE,CAAC;AACF,KAAK,QAAQ,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,IAAI,QAAQ,CACjE,CAAC,GAAG,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,CACnB,CAAC;AACF,KAAK,eAAe,CAClB,CAAC,SAAS,SAAS,EACnB,CAAC,SAAS,UAAU,EACpB,CAAC,SAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,IACtB,CAAC,GAAG;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEpC,KAAK,aAAa,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,UAAU,IAAI,CAAC,CAC/D,KAAK,CAAC,EAAE,SAAS,CAAC,EAElB,GAAG,IAAI,EAAE,EAAE,KACR,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG;IACtB,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EACnC,GAAG,EAAE,CAAC,KACH,QAAQ,CAAC,CAAC,GAAG,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC5C,MAAM,EAAE,CAAC,KAAK,CAAC,EAAE,SAAS,CAAC,OAAO,aAAa,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACvD,UAAU,EAAE,CACV,KAAK,CAAC,CAAC,SAAS,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAG9B,GAAG,KAAK,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,EAAE,KACzB,aAAa,CAAC;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,EAAE,CAAC,CAAC,CAAC;CACpD,CAAC;AA4CF,eAAO,MAAM,WAAW,6DAIvB,CAAC;AAEF,eAAO,MAAM,KAAK,UAvFS,EAAE;aAEX,EAAE;;;;;;;;;;;;;;;;;;;;;CAqFyB,CAAC;AAE9C,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,SAAS,MAAM,EAAE,EAAE,QAAQ,CAAC,gCAelE,CAAC"}
package/dist/index.js CHANGED
@@ -9,41 +9,36 @@ function arrayEquivalent(a1, a2) {
9
9
  }
10
10
  return true;
11
11
  }
12
- function recordEquivalent(r1, r2) {
13
- const r1Keys = Object.keys(r1);
14
- const r2Keys = Object.keys(r2);
15
- if (r2Keys.length !== r1Keys.length)
16
- return false;
17
- for (let i = 0; i < r1Keys.length; i++) {
18
- const k = r1Keys[i];
19
- if (!(k in r2))
20
- return false;
21
- if (!Object.is(r2[k], r1[k]))
22
- return false;
23
- }
24
- return true;
25
- }
26
- export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent, (x) => Object.freeze(x));
27
- export const _recordCache = new InternCache((r) => hash.valuesHash(Object.keys(r)
28
- .sort()
29
- .flatMap((k) => [k, r[k]])), recordEquivalent, (x) => Object.freeze(x));
30
- export const tuple = (...t) => _tupleCache.get(t);
31
- export const record = ({ ...r }) => _recordCache.get(r);
32
- export class DuplicateKeyError extends Error {
33
- constructor() {
34
- super("All keys must be distinct.");
35
- this.name = "DuplicateKeyError";
36
- }
37
- }
38
- export const struct = (...keys) => {
39
- keys.forEach((k) => {
40
- if (typeof k !== "string") {
41
- throw new TypeError("All keys must be strings.");
42
- }
43
- });
44
- if (new Set(keys).size !== keys.length) {
45
- throw new DuplicateKeyError();
46
- }
47
- const cache = new InternCache((s) => hash.valuesHash(keys.map((k) => s[k])), (s1, s2) => keys.every((k) => Object.is(s1[k], s2[k])), (r) => Object.fromEntries(keys.map((k) => [k, r[k]])));
48
- return (s) => cache.get(Object.fromEntries(keys.map((k) => [k, s[k]])));
12
+ const defineTuple = () => {
13
+ const construct = (...args) => _tupleCache.get(args);
14
+ const narrow = (..._args) => defineTuple();
15
+ return Object.assign(construct, { narrow });
16
+ };
17
+ const defineStruct = (cache, labels) => {
18
+ const construct = (...args) => {
19
+ return cache.get(args);
20
+ };
21
+ const fromTuple = (...args) => {
22
+ return construct(...args);
23
+ };
24
+ const fromObject = (arg) => {
25
+ const args = labels.map((label) => arg[label]);
26
+ return construct(...args);
27
+ };
28
+ const narrow = (..._args) => defineStruct(cache, labels);
29
+ const narrowFrom = (..._args) => defineStruct(cache, labels);
30
+ return Object.assign(fromTuple, { from: fromObject, narrow, narrowFrom });
31
+ };
32
+ export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent, Object.freeze);
33
+ export const tuple = defineTuple();
34
+ export const struct = (labels) => {
35
+ const cacheInjestFn = (tuple) => {
36
+ const labeledTuple = tuple;
37
+ labels.forEach((label, index) => {
38
+ labeledTuple[label] = tuple[index];
39
+ });
40
+ return Object.freeze(labeledTuple);
41
+ };
42
+ const cache = new InternCache(hash.valuesHash, arrayEquivalent, cacheInjestFn);
43
+ return defineStruct(cache, labels);
49
44
  };
@@ -1,12 +1,12 @@
1
- export default class<H, T extends object> {
1
+ export default class<H, K, V extends object> {
2
2
  private readonly hashFn;
3
3
  private readonly matchFn;
4
4
  private readonly injestFn;
5
5
  private readonly map;
6
- private cleanupRegistry;
7
- constructor(hashFn: (value: T) => H, matchFn: (key: T, target: T) => boolean, injestFn: <T_ extends T>(value: T_) => T_);
6
+ private readonly cleanupRegistry;
7
+ constructor(hashFn: (key: K) => H, matchFn: (key: K, value: V) => boolean, injestFn: (key: K) => V);
8
8
  private remove;
9
- get<T_ extends T>(key: T_): T_;
9
+ get<K_ extends K, V_ extends V>(key: K_): V_;
10
10
  get size(): number;
11
11
  reset(): void;
12
12
  }
@@ -1 +1 @@
1
- {"version":3,"file":"intern-cache.d.ts","sourceRoot":"","sources":["../src/intern-cache.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAO,CAAC,EAAE,CAAC,SAAS,MAAM;IAOpC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAR3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,eAAe,CAErB;gBAGiB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EACvB,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,KAAK,OAAO,EACvC,QAAQ,EAAE,CAAC,EAAE,SAAS,CAAC,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE;IAG5D,OAAO,CAAC,MAAM;IAWd,GAAG,CAAC,EAAE,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE;IAsB9B,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,KAAK,IAAI,IAAI;CAId"}
1
+ {"version":3,"file":"intern-cache.d.ts","sourceRoot":"","sources":["../src/intern-cache.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,SAAS,MAAM;IAOvC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAR3B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAE9B;gBAGiB,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EACrB,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,OAAO,EACtC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC;IAG1C,OAAO,CAAC,MAAM;IAWd,GAAG,CAAC,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE;IAsB5C,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,KAAK,IAAI,IAAI;CAId"}
@@ -6,12 +6,12 @@ export default class {
6
6
  this.map = new Map();
7
7
  this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
8
8
  }
9
- remove([hash, ref]) {
9
+ remove([hash, valueRef]) {
10
10
  const hashBucket = this.map.get(hash);
11
11
  if (!hashBucket) {
12
12
  return;
13
13
  }
14
- hashBucket.delete(ref);
14
+ hashBucket.delete(valueRef);
15
15
  if (hashBucket.size === 0) {
16
16
  this.map.delete(hash);
17
17
  }
@@ -32,17 +32,17 @@ export default class {
32
32
  this.map.set(hash, hashBucket);
33
33
  }
34
34
  // Object is not in the cache. Add it and return it.
35
- const obj = this.injestFn(key);
36
- const ref = new WeakRef(obj);
37
- hashBucket.add(ref);
38
- this.cleanupRegistry.register(obj, [hash, ref]);
39
- return obj;
35
+ const value = this.injestFn(key);
36
+ const valueRef = new WeakRef(value);
37
+ hashBucket.add(valueRef);
38
+ this.cleanupRegistry.register(value, [hash, valueRef], this);
39
+ return value;
40
40
  }
41
41
  get size() {
42
42
  return Array.from(this.map.entries(), ([_, b]) => b.size).reduce((acc, x) => acc + x, 0);
43
43
  }
44
44
  reset() {
45
+ this.cleanupRegistry.unregister(this);
45
46
  this.map.clear();
46
- this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
47
47
  }
48
48
  }
package/package.json CHANGED
@@ -1,11 +1,18 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "tuple-ts",
4
- "version": "0.5.0",
4
+ "version": "0.7.0",
5
5
  "description": "Tuples and structs for TypeScript and JavaScript with equality and identity semantics.",
6
6
  "keywords": [
7
7
  "tuple",
8
8
  "struct",
9
+ "labeled",
10
+ "labelled",
11
+ "labels",
12
+ "names",
13
+ "namedtuple",
14
+ "named-tuple",
15
+ "python",
9
16
  "equality",
10
17
  "identity",
11
18
  "shallow-equality",