tuple-ts 0.5.0 → 0.6.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,13 +1,13 @@
1
1
  # tuple-ts
2
2
 
3
- **Immutable tuples and structs with value-based identity for JavaScript and TypeScript.**
3
+ **Immutable tuples, records and structs with value-based identity for JavaScript and TypeScript.**
4
4
 
5
5
  JavaScript normally compares arrays and objects by reference:
6
6
 
7
7
  ```ts
8
8
  [1, 2] === [1, 2] // false
9
9
 
10
- { x: 1 } === { x: 1 } // false
10
+ ({ x: 1 }) === ({ x: 1 }) // false
11
11
  ```
12
12
 
13
13
  `tuple-ts` gives you a different model:
@@ -20,7 +20,7 @@ record({ x: 1 }) === record({ x: 1 }) // true
20
20
 
21
21
  Equal values are **interned to the same object**.
22
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.
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
24
 
25
25
  **Zero dependencies · Less than 1 KB gzipped**
26
26
 
@@ -148,12 +148,31 @@ a === c; // false
148
148
 
149
149
  ## Typed tuples
150
150
 
151
- You can create a reusable constructor for a particular tuple type:
151
+ Use `tuple.like()` to create a reusable constructor for a particular tuple type.
152
+
153
+ ### Inferring the type from an exemplar
154
+
155
+ ```ts
156
+ const Point = tuple.like(0, 0);
157
+
158
+ const a = Point(10, 20);
159
+ const b = Point(10, 20);
160
+
161
+ a === b; // true
162
+ ```
163
+
164
+ The exemplar determines the TypeScript type expected by the resulting factory. Its values are not used as defaults.
165
+
166
+ By default, TypeScript widens the exemplar's literal types, so `tuple.like(0, 0)` produces a factory accepting two numbers.
167
+
168
+ ### Explicit type arguments
169
+
170
+ Alternatively, specify the type directly:
152
171
 
153
172
  ```ts
154
173
  type Point = [x: number, y: number];
155
174
 
156
- const Point = tuple<Point>;
175
+ const Point = tuple.like<Point>();
157
176
 
158
177
  const a = Point(10, 20);
159
178
  const b = Point(10, 20);
@@ -161,7 +180,20 @@ const b = Point(10, 20);
161
180
  a === b; // true
162
181
  ```
163
182
 
164
- The result retains the tuple's TypeScript type while gaining the same runtime identity semantics.
183
+ No exemplar is required when the type argument is provided explicitly.
184
+
185
+ ### Preserving literal types
186
+
187
+ To preserve literal types rather than widen them, use `as const`:
188
+
189
+ ```ts
190
+ const Direction = tuple.like("north" as const, 1 as const);
191
+
192
+ Direction("north", 1); // OK
193
+ Direction("south", 1); // TypeScript error
194
+ ```
195
+
196
+ In all cases, the resulting tuples are frozen and interned using the same shared cache as ordinary tuples.
165
197
 
166
198
  ## Records
167
199
 
@@ -206,9 +238,27 @@ users.get(
206
238
  ); // "Alice"
207
239
  ```
208
240
 
241
+ ### Record identity
242
+
243
+ A record's identity is determined by its own, enumerable, string-keyed properties and their values.
244
+
245
+ Two records are equivalent if they contain exactly the same keys, with equivalent values, regardless of property order.
246
+
247
+ ```ts
248
+ record({ x: 1, y: 2 }) ===
249
+ record({ y: 2, x: 1 }); // true
250
+
251
+ record({ x: 1 }) ===
252
+ record({ x: 1, y: 2 }); // false
253
+ ```
254
+
255
+ Symbol-keyed, inherited and non-enumerable properties are not copied into records.
256
+
257
+ Numeric property keys are supported and follow JavaScript's normal conversion to string keys.
258
+
209
259
  ### Typed records
210
260
 
211
- As with tuples, you can define a reusable typed constructor by passing a type argument:
261
+ Use `record.like()` to create a reusable typed record constructor:
212
262
 
213
263
  ```ts
214
264
  interface User {
@@ -217,7 +267,7 @@ interface User {
217
267
  updatedAt: number;
218
268
  }
219
269
 
220
- const User = record<User>;
270
+ const User = record.like<User>();
221
271
 
222
272
  const a = User({
223
273
  id: "alice",
@@ -234,11 +284,36 @@ const b = User({
234
284
  a === b; // true
235
285
  ```
236
286
 
287
+ Alternatively, infer the type from an exemplar:
288
+
289
+ ```ts
290
+ const User = record.like({
291
+ id: "",
292
+ name: "",
293
+ updatedAt: 0,
294
+ });
295
+ ```
296
+
297
+ As with `tuple.like()`, the exemplar defines the type; it does not supply default values.
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.
302
+
303
+ ```ts
304
+ const User = record.like({ id: "" });
305
+
306
+ const a = User({ id: "alice" });
307
+ const b = User({ id: "alice", role: "admin" });
308
+
309
+ a === b; // false
310
+ ```
311
+
237
312
  ## Structs
238
313
 
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`.
314
+ `record` interns whatever properties you give it. If you want a fixed set of keys to define identity, use `struct`.
240
315
 
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.
316
+ `struct` takes key names as arguments and returns a factory. The factory accepts any object with at least those keys, extracts **exactly the declared keys**, and interns the result.
242
317
 
243
318
  ```ts
244
319
  import { struct } from "tuple-ts";
@@ -251,20 +326,109 @@ const b = Point({ x: 1, y: 2 });
251
326
  a === b; // true
252
327
  ```
253
328
 
254
- Because the schema is fixed at construction time, equality is always based on precisely those keys — no more, no less:
329
+ ### Additional properties are discarded
330
+
331
+ Unlike `record`, a struct factory ignores properties that are not part of its declared structure:
255
332
 
256
333
  ```ts
334
+ const Point = struct("x", "y");
335
+
257
336
  const a = Point({ x: 1, y: 2 });
258
- const b = Point({ x: 1, y: 3 });
337
+ const b = Point({ x: 1, y: 2, z: 3 });
338
+
339
+ a === b; // true
340
+ ```
341
+
342
+ The resulting objects contain only `x` and `y`:
343
+
344
+ ```ts
345
+ Object.keys(b); // ["x", "y"]
259
346
 
260
- a === b; // false — y differs
347
+ b.z; // TypeScript error
261
348
  ```
262
349
 
263
- All keys passed to `struct` must be distinct strings. Passing duplicate keys throws a `DuplicateKeyError`.
350
+ This makes structs useful for projecting larger objects into canonical values with a precisely defined shape.
351
+
352
+ ```ts
353
+ const Position = struct("x", "y");
354
+
355
+ const location = {
356
+ x: 10,
357
+ y: 20,
358
+ altitude: 100,
359
+ timestamp: Date.now(),
360
+ };
361
+
362
+ const position = Position(location);
363
+ // { x: 10, y: 20 }
364
+ ```
365
+
366
+ The factory is deliberately permissive about additional input properties, but strict about which properties appear in its output.
367
+
368
+ ### Typed structs
369
+
370
+ Use `struct(...keys).like()` to constrain the types of the declared properties.
371
+
372
+ The type can be inferred from an exemplar:
373
+
374
+ ```ts
375
+ const Point = struct("x", "y").like({
376
+ x: 0,
377
+ y: 0,
378
+ });
379
+
380
+ Point({ x: 10, y: 20 }); // OK
381
+ Point({ x: "10", y: 20 }); // TypeScript error
382
+ ```
383
+
384
+ Or provided explicitly:
385
+
386
+ ```ts
387
+ type Coordinates = {
388
+ x: number;
389
+ y: number;
390
+ };
391
+
392
+ const Point = struct("x", "y").like<Coordinates>();
393
+
394
+ Point({ x: 10, y: 20 }); // OK
395
+ ```
396
+
397
+ As with the other factories, `like()` defines types, not default values.
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`:
404
+
405
+ ```ts
406
+ const Point = struct("x", "y");
407
+
408
+ Point.like({
409
+ x: 0,
410
+ y: 0,
411
+ z: 0, // TypeScript error: unexpected property
412
+ });
413
+ ```
414
+
415
+ But the resulting factory accepts extra properties and discards them:
416
+
417
+ ```ts
418
+ const Point = struct("x", "y").like({
419
+ x: 0,
420
+ y: 0,
421
+ });
422
+
423
+ Point({ x: 10, y: 20, z: 30 }); // OK
424
+ // { x: 10, y: 20 }
425
+ ```
426
+
427
+ This keeps struct definitions precise without making callers manually remove unrelated properties from their input objects.
264
428
 
265
429
  ## Equality semantics
266
430
 
267
- Primitive values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
431
+ Values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
268
432
 
269
433
  ```ts
270
434
  tuple(NaN) === tuple(NaN); // true
@@ -290,19 +454,54 @@ tuple({ x: 1 }) === tuple({ x: 1 }); // false
290
454
 
291
455
  This is **shallow equality**. `tuple-ts` does not recursively compare arbitrary JavaScript objects.
292
456
 
293
- Nested tuples and structs work particularly well because they are themselves canonical values.
457
+ Nested tuples, records and structs work particularly well because they are themselves canonical values:
458
+
459
+ ```ts
460
+ const a = record({
461
+ position: tuple(10, 20),
462
+ });
463
+
464
+ const b = record({
465
+ position: tuple(10, 20),
466
+ });
467
+
468
+ a === b; // true
469
+ ```
470
+
471
+ The outer records compare equal because the inner tuples have already been interned to the same object.
472
+
473
+ All three constructors share this model of identity.
294
474
 
295
475
  ## Immutable by design
296
476
 
297
- Tuples and structs returned by `tuple-ts` are frozen:
477
+ Tuples, records and structs returned by `tuple-ts` are frozen:
298
478
 
299
479
  ```ts
300
480
  const point = tuple(10, 20);
481
+ const user = record({ id: 1 });
482
+ const position = struct("x", "y")({ x: 1, y: 2 });
301
483
 
302
- Object.isFrozen(point); // true
484
+ Object.isFrozen(point); // true
485
+ Object.isFrozen(user); // true
486
+ Object.isFrozen(position); // true
303
487
  ```
304
488
 
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.
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.
306
505
 
307
506
  ## Garbage-collection friendly
308
507
 
@@ -310,40 +509,86 @@ Interning values creates a natural question: *doesn't the cache grow forever?*
310
509
 
311
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.
312
511
 
313
- In other words, you don't have to manually maintain a global cache of every tuple you've ever created.
512
+ In other words, you don't have to manually maintain a global cache of every tuple, record or struct you've ever created.
314
513
 
315
514
  ## TypeScript first
316
515
 
317
- `tuple`, `record`, and `struct` are designed to preserve TypeScript's inference:
516
+ `tuple`, `record` and `struct` are designed to preserve TypeScript's inference.
318
517
 
319
- ```ts
320
- const value = tuple(
321
- "hello",
322
- 42,
323
- true,
324
- );
518
+ ### Literal inference
519
+
520
+ By default, constructors preserve literal types:
325
521
 
522
+ ```ts
523
+ const t = tuple("hello", 42, true);
326
524
  // readonly ["hello", 42, true]
525
+
526
+ const r = record({ x: 1, y: 2 });
527
+ // readonly properties x: 1, y: 2
327
528
  ```
328
529
 
329
- The library is deliberately small: the runtime API consists of three constructors:
530
+ ### Reusable typed factories
531
+
532
+ Use `.like()` to define reusable constructors:
330
533
 
331
534
  ```ts
332
- import { tuple, record, struct } from "tuple-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
+ });
333
546
  ```
334
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
+
335
572
  ## When should I use it?
336
573
 
337
574
  `tuple-ts` is particularly useful when you want **small immutable values with stable identity**:
338
575
 
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
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 |
347
592
 
348
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**.
349
594
 
@@ -351,10 +596,14 @@ If you just need an ordinary mutable array or object, use one. `tuple-ts` is for
351
596
 
352
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.
353
598
 
599
+ Property order is ignored for records and structs, but element order matters for tuples.
600
+
354
601
  The intern cache uses weak references, so canonical values do not need to be kept alive solely because they have previously been created.
355
602
 
603
+ Constructed records and structs are copied before interning, so callers cannot alter an interned value by modifying the source object.
604
+
356
605
  There are no runtime dependencies.
357
606
 
358
607
  ## License
359
608
 
360
- ISC
609
+ ISC
package/dist/index.d.ts CHANGED
@@ -1,11 +1,78 @@
1
1
  import InternCache from "./intern-cache";
2
+ type NoExtraKeys<T, K extends PropertyKey> = T & Record<Exclude<keyof T, K>, never>;
3
+ type BaseKey = string | number;
4
+ type BaseTuple = readonly unknown[];
5
+ type BaseRecord<K extends BaseKey> = Record<K, unknown>;
2
6
  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]>;
7
+ export declare const _recordCache: InternCache<number, Record<BaseKey, unknown>>;
8
+ export declare const tuple: (<const T_ extends BaseTuple>(...t: T_) => Readonly<T_>) & {
9
+ like: <T_ extends BaseTuple>(...exemplar: [] | T_) => (<const T__1 extends T_>(...t: T__1) => Readonly<T__1>) & {
10
+ like: <T__1 extends T_>(...exemplar: [] | T__1) => (<const T__2 extends T__1>(...t: T__2) => Readonly<T__2>) & {
11
+ like: <T__2 extends T__1>(...exemplar: [] | T__2) => (<const T__3 extends T__2>(...t: T__3) => Readonly<T__3>) & {
12
+ like: <T__3 extends T__2>(...exemplar: [] | T__3) => (<const T__4 extends T__3>(...t: T__4) => Readonly<T__4>) & {
13
+ like: <T__4 extends T__3>(...exemplar: [] | T__4) => (<const T__5 extends T__4>(...t: T__5) => Readonly<T__5>) & {
14
+ like: <T__5 extends T__4>(...exemplar: [] | T__5) => (<const T__6 extends T__5>(...t: T__6) => Readonly<T__6>) & {
15
+ like: <T__6 extends T__5>(...exemplar: [] | T__6) => (<const T__7 extends T__6>(...t: T__7) => Readonly<T__7>) & {
16
+ like: <T__7 extends T__6>(...exemplar: [] | T__7) => (<const T__8 extends T__7>(...t: T__8) => Readonly<T__8>) & {
17
+ like: <T__8 extends T__7>(...exemplar: [] | T__8) => (<const T__9 extends T__8>(...t: T__9) => Readonly<T__9>) & {
18
+ like: <T__9 extends T__8>(...exemplar: [] | T__9) => (<const T__10 extends T__9>(...t: T__10) => Readonly<T__10>) & {
19
+ like: <T__10 extends T__9>(...exemplar: [] | T__10) => (<const T__11 extends T__10>(...t: T__11) => Readonly<T__11>) & /*elided*/ any;
20
+ };
21
+ };
22
+ };
23
+ };
24
+ };
25
+ };
26
+ };
27
+ };
28
+ };
29
+ };
30
+ };
31
+ export declare const record: (<const R_ extends BaseRecord<BaseKey>>(r: R_) => Readonly<Pick<R_, Extract<keyof R_, BaseKey>>>) & {
32
+ like: <R_ extends BaseRecord<BaseKey>>(exemplar?: NoExtraKeys<R_, BaseKey> | undefined) => (<const R__1 extends R_>(r: R__1) => Readonly<Pick<R__1, Extract<keyof R__1, BaseKey>>>) & {
33
+ like: <R__1 extends R_>(exemplar?: NoExtraKeys<R__1, BaseKey> | undefined) => (<const R__2 extends R__1>(r: R__2) => Readonly<Pick<R__2, Extract<keyof R__2, BaseKey>>>) & {
34
+ like: <R__2 extends R__1>(exemplar?: NoExtraKeys<R__2, BaseKey> | undefined) => (<const R__3 extends R__2>(r: R__3) => Readonly<Pick<R__3, Extract<keyof R__3, BaseKey>>>) & {
35
+ like: <R__3 extends R__2>(exemplar?: NoExtraKeys<R__3, BaseKey> | undefined) => (<const R__4 extends R__3>(r: R__4) => Readonly<Pick<R__4, Extract<keyof R__4, BaseKey>>>) & {
36
+ like: <R__4 extends R__3>(exemplar?: NoExtraKeys<R__4, BaseKey> | undefined) => (<const R__5 extends R__4>(r: R__5) => Readonly<Pick<R__5, Extract<keyof R__5, BaseKey>>>) & {
37
+ like: <R__5 extends R__4>(exemplar?: NoExtraKeys<R__5, BaseKey> | undefined) => (<const R__6 extends R__5>(r: R__6) => Readonly<Pick<R__6, Extract<keyof R__6, BaseKey>>>) & {
38
+ like: <R__6 extends R__5>(exemplar?: NoExtraKeys<R__6, BaseKey> | undefined) => (<const R__7 extends R__6>(r: R__7) => Readonly<Pick<R__7, Extract<keyof R__7, BaseKey>>>) & {
39
+ like: <R__7 extends R__6>(exemplar?: NoExtraKeys<R__7, BaseKey> | undefined) => (<const R__8 extends R__7>(r: R__8) => Readonly<Pick<R__8, Extract<keyof R__8, BaseKey>>>) & {
40
+ like: <R__8 extends R__7>(exemplar?: NoExtraKeys<R__8, BaseKey> | undefined) => (<const R__9 extends R__8>(r: R__9) => Readonly<Pick<R__9, Extract<keyof R__9, BaseKey>>>) & {
41
+ like: <R__9 extends R__8>(exemplar?: NoExtraKeys<R__9, BaseKey> | undefined) => (<const R__10 extends R__9>(r: R__10) => Readonly<Pick<R__10, Extract<keyof R__10, BaseKey>>>) & {
42
+ like: <R__10 extends R__9>(exemplar?: NoExtraKeys<R__10, BaseKey> | undefined) => (<const R__11 extends R__10>(r: R__11) => Readonly<Pick<R__11, Extract<keyof R__11, BaseKey>>>) & /*elided*/ any;
43
+ };
44
+ };
45
+ };
46
+ };
47
+ };
48
+ };
49
+ };
50
+ };
51
+ };
52
+ };
53
+ };
54
+ export declare const struct: <const K extends string>(...keys: readonly K[]) => (<const R_ extends BaseRecord<K>>(r: R_) => Readonly<Pick<R_, Extract<keyof R_, K>>>) & {
55
+ like: <R_ extends BaseRecord<K>>(exemplar?: NoExtraKeys<R_, K> | undefined) => (<const R__1 extends R_>(r: R__1) => Readonly<Pick<R__1, Extract<keyof R__1, K>>>) & {
56
+ like: <R__1 extends R_>(exemplar?: NoExtraKeys<R__1, K> | undefined) => (<const R__2 extends R__1>(r: R__2) => Readonly<Pick<R__2, Extract<keyof R__2, K>>>) & {
57
+ like: <R__2 extends R__1>(exemplar?: NoExtraKeys<R__2, K> | undefined) => (<const R__3 extends R__2>(r: R__3) => Readonly<Pick<R__3, Extract<keyof R__3, K>>>) & {
58
+ like: <R__3 extends R__2>(exemplar?: NoExtraKeys<R__3, K> | undefined) => (<const R__4 extends R__3>(r: R__4) => Readonly<Pick<R__4, Extract<keyof R__4, K>>>) & {
59
+ like: <R__4 extends R__3>(exemplar?: NoExtraKeys<R__4, K> | undefined) => (<const R__5 extends R__4>(r: R__5) => Readonly<Pick<R__5, Extract<keyof R__5, K>>>) & {
60
+ like: <R__5 extends R__4>(exemplar?: NoExtraKeys<R__5, K> | undefined) => (<const R__6 extends R__5>(r: R__6) => Readonly<Pick<R__6, Extract<keyof R__6, K>>>) & {
61
+ like: <R__6 extends R__5>(exemplar?: NoExtraKeys<R__6, K> | undefined) => (<const R__7 extends R__6>(r: R__7) => Readonly<Pick<R__7, Extract<keyof R__7, K>>>) & {
62
+ like: <R__7 extends R__6>(exemplar?: NoExtraKeys<R__7, K> | undefined) => (<const R__8 extends R__7>(r: R__8) => Readonly<Pick<R__8, Extract<keyof R__8, K>>>) & {
63
+ like: <R__8 extends R__7>(exemplar?: NoExtraKeys<R__8, K> | undefined) => (<const R__9 extends R__8>(r: R__9) => Readonly<Pick<R__9, Extract<keyof R__9, K>>>) & {
64
+ like: <R__9 extends R__8>(exemplar?: NoExtraKeys<R__9, K> | undefined) => (<const R__10 extends R__9>(r: R__10) => Readonly<Pick<R__10, Extract<keyof R__10, K>>>) & {
65
+ like: <R__10 extends R__9>(exemplar?: NoExtraKeys<R__10, K> | undefined) => (<const R__11 extends R__10>(r: R__11) => Readonly<Pick<R__11, Extract<keyof R__11, K>>>) & /*elided*/ any;
66
+ };
67
+ };
68
+ };
69
+ };
70
+ };
71
+ };
72
+ };
73
+ };
74
+ };
75
+ };
76
+ };
77
+ export {};
11
78
  //# 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,WAAW,CAAC,CAAC,EAAE,CAAC,SAAS,WAAW,IAAI,CAAC,GAC5C,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;AAErC,KAAK,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC/B,KAAK,SAAS,GAAG,SAAS,OAAO,EAAE,CAAC;AACpC,KAAK,UAAU,CAAC,CAAC,SAAS,OAAO,IAAI,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AA+CxD,eAAO,MAAM,WAAW,yCAAoD,CAAC;AAE7E,eAAO,MAAM,YAAY,+CAQxB,CAAC;AAEF,eAAO,MAAM,KAAK,UAhCP,EAAE;WAEA,EAAE;;;;;;;;;;;;;;;;;;;;;CA8BmB,CAAC;AAEnC,eAAO,MAAM,MAAM,UAxBR,EAAE;WAKA,EAAE;;;;;;;;;;;;;;;;;;;;;CAmBqB,CAAC;AAErC,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,EAAE,aA1BzD,EAAE;WAKA,EAAE;;;;;;;;;;;;;;;;;;;;;CAsBK,CAAC"}
package/dist/index.js CHANGED
@@ -14,36 +14,18 @@ function recordEquivalent(r1, r2) {
14
14
  const r2Keys = Object.keys(r2);
15
15
  if (r2Keys.length !== r1Keys.length)
16
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;
17
+ return r1Keys.every(k => Object.hasOwn(r2, k) && Object.is(r1[k], r2[k]));
25
18
  }
26
- export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent, (x) => Object.freeze(x));
19
+ const defineTuple = () => Object.assign((...t) => _tupleCache.get(t), {
20
+ like: (...exemplar) => defineTuple(),
21
+ });
22
+ const defineRecord = (keys) => Object.assign((r) => _recordCache.get(Object.fromEntries(keys ? keys.map((k) => [k, r[k]]) : Object.entries(r))), {
23
+ like: (exemplar) => defineRecord(keys),
24
+ });
25
+ export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent);
27
26
  export const _recordCache = new InternCache((r) => hash.valuesHash(Object.keys(r)
28
27
  .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]])));
49
- };
28
+ .flatMap((k) => [k, r[k]])), recordEquivalent);
29
+ export const tuple = defineTuple();
30
+ export const record = defineRecord();
31
+ export const struct = (...keys) => defineRecord(keys);
@@ -1,12 +1,11 @@
1
1
  export default class<H, T extends object> {
2
2
  private readonly hashFn;
3
3
  private readonly matchFn;
4
- private readonly injestFn;
5
4
  private readonly map;
6
5
  private cleanupRegistry;
7
- constructor(hashFn: (value: T) => H, matchFn: (key: T, target: T) => boolean, injestFn: <T_ extends T>(value: T_) => T_);
6
+ constructor(hashFn: (value: T) => H, matchFn: (a: T, b: T) => boolean);
8
7
  private remove;
9
- get<T_ extends T>(key: T_): T_;
8
+ get<T_ extends T>(obj: T_): Readonly<T_>;
10
9
  get size(): number;
11
10
  reset(): void;
12
11
  }
@@ -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,SAAS,MAAM;IAOpC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAP1B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,eAAe,CAErB;gBAGiB,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,EACvB,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO;IAGnD,OAAO,CAAC,MAAM;IAWd,GAAG,CAAC,EAAE,SAAS,CAAC,EAAE,GAAG,EAAE,EAAE,GAAG,QAAQ,CAAC,EAAE,CAAC;IAsBxC,IAAI,IAAI,IAAI,MAAM,CAKjB;IAED,KAAK,IAAI,IAAI;CAId"}
@@ -1,8 +1,7 @@
1
1
  export default class {
2
- constructor(hashFn, matchFn, injestFn) {
2
+ constructor(hashFn, matchFn) {
3
3
  this.hashFn = hashFn;
4
4
  this.matchFn = matchFn;
5
- this.injestFn = injestFn;
6
5
  this.map = new Map();
7
6
  this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
8
7
  }
@@ -16,13 +15,13 @@ export default class {
16
15
  this.map.delete(hash);
17
16
  }
18
17
  }
19
- get(key) {
20
- const hash = this.hashFn(key);
18
+ get(obj) {
19
+ const hash = this.hashFn(obj);
21
20
  let hashBucket = this.map.get(hash);
22
21
  if (hashBucket !== undefined) {
23
22
  for (const ref of hashBucket) {
24
23
  const o = ref.deref();
25
- if (o && this.matchFn(key, o))
24
+ if (o && this.matchFn(obj, o))
26
25
  return o;
27
26
  }
28
27
  }
@@ -32,7 +31,7 @@ export default class {
32
31
  this.map.set(hash, hashBucket);
33
32
  }
34
33
  // Object is not in the cache. Add it and return it.
35
- const obj = this.injestFn(key);
34
+ Object.freeze(obj);
36
35
  const ref = new WeakRef(obj);
37
36
  hashBucket.add(ref);
38
37
  this.cleanupRegistry.register(obj, [hash, ref]);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "tuple-ts",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "description": "Tuples and structs for TypeScript and JavaScript with equality and identity semantics.",
6
6
  "keywords": [
7
7
  "tuple",