tuple-ts 0.4.7 → 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:
@@ -15,12 +15,12 @@ JavaScript normally compares arrays and objects by reference:
15
15
  ```ts
16
16
  tuple(1, 2) === tuple(1, 2) // true
17
17
 
18
- struct({ x: 1 }) === struct({ x: 1 }) // true
18
+ record({ x: 1 }) === record({ x: 1 }) // true
19
19
  ```
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,21 +180,34 @@ 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.
165
184
 
166
- ## Structs
185
+ ### Preserving literal types
167
186
 
168
- `struct` provides the same semantics for objects:
187
+ To preserve literal types rather than widen them, use `as const`:
169
188
 
170
189
  ```ts
171
- import { struct } from "tuple-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.
197
+
198
+ ## Records
199
+
200
+ `record` provides the same value-based identity semantics for objects:
172
201
 
173
- const a = struct({
202
+ ```ts
203
+ import { record } from "tuple-ts";
204
+
205
+ const a = record({
174
206
  id: 1,
175
207
  role: "admin",
176
208
  });
177
209
 
178
- const b = struct({
210
+ const b = record({
179
211
  role: "admin",
180
212
  id: 1,
181
213
  });
@@ -185,7 +217,7 @@ a === b; // true
185
217
 
186
218
  Property order does not matter.
187
219
 
188
- Structs are frozen:
220
+ Records are frozen:
189
221
 
190
222
  ```ts
191
223
  Object.isFrozen(a); // true
@@ -197,18 +229,36 @@ And they work naturally as `Map` keys:
197
229
  const users = new Map();
198
230
 
199
231
  users.set(
200
- struct({ id: 42, role: "admin" }),
232
+ record({ id: 42, role: "admin" }),
201
233
  "Alice",
202
234
  );
203
235
 
204
236
  users.get(
205
- struct({ role: "admin", id: 42 }),
237
+ record({ role: "admin", id: 42 }),
206
238
  ); // "Alice"
207
239
  ```
208
240
 
209
- ### Typed structs
241
+ ### Record identity
210
242
 
211
- As with tuples, you can define a reusable typed constructor:
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
+
259
+ ### Typed records
260
+
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 = struct<User>;
270
+ const User = record.like<User>();
221
271
 
222
272
  const a = User({
223
273
  id: "alice",
@@ -234,9 +284,151 @@ 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
+
312
+ ## Structs
313
+
314
+ `record` interns whatever properties you give it. If you want a fixed set of keys to define identity, use `struct`.
315
+
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.
317
+
318
+ ```ts
319
+ import { struct } from "tuple-ts";
320
+
321
+ const Point = struct("x", "y");
322
+
323
+ const a = Point({ x: 1, y: 2 });
324
+ const b = Point({ x: 1, y: 2 });
325
+
326
+ a === b; // true
327
+ ```
328
+
329
+ ### Additional properties are discarded
330
+
331
+ Unlike `record`, a struct factory ignores properties that are not part of its declared structure:
332
+
333
+ ```ts
334
+ const Point = struct("x", "y");
335
+
336
+ const a = Point({ x: 1, y: 2 });
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"]
346
+
347
+ b.z; // TypeScript error
348
+ ```
349
+
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.
428
+
237
429
  ## Equality semantics
238
430
 
239
- 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):
240
432
 
241
433
  ```ts
242
434
  tuple(NaN) === tuple(NaN); // true
@@ -262,19 +454,54 @@ tuple({ x: 1 }) === tuple({ x: 1 }); // false
262
454
 
263
455
  This is **shallow equality**. `tuple-ts` does not recursively compare arbitrary JavaScript objects.
264
456
 
265
- 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.
266
474
 
267
475
  ## Immutable by design
268
476
 
269
- Tuples and structs returned by `tuple-ts` are frozen:
477
+ Tuples, records and structs returned by `tuple-ts` are frozen:
270
478
 
271
479
  ```ts
272
480
  const point = tuple(10, 20);
481
+ const user = record({ id: 1 });
482
+ const position = struct("x", "y")({ x: 1, y: 2 });
483
+
484
+ Object.isFrozen(point); // true
485
+ Object.isFrozen(user); // true
486
+ Object.isFrozen(position); // true
487
+ ```
488
+
489
+ This is important to the identity semantics: once a value has been interned, its own properties cannot subsequently change while it is being used as a `Map` key or `Set` member.
490
+
491
+ Freezing is shallow. If an interned value contains a reference to an ordinary mutable object, that nested object is not automatically frozen.
492
+
493
+ For example:
494
+
495
+ ```ts
496
+ const details = { name: "Alice" };
497
+ const user = record({ details });
273
498
 
274
- Object.isFrozen(point); // true
499
+ details.name = "Bob"; // Allowed
275
500
  ```
276
501
 
277
- 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.
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.
278
505
 
279
506
  ## Garbage-collection friendly
280
507
 
@@ -282,40 +509,86 @@ Interning values creates a natural question: *doesn't the cache grow forever?*
282
509
 
283
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.
284
511
 
285
- 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.
286
513
 
287
514
  ## TypeScript first
288
515
 
289
- `tuple` and `struct` are designed to preserve TypeScript's inference:
516
+ `tuple`, `record` and `struct` are designed to preserve TypeScript's inference.
290
517
 
291
- ```ts
292
- const value = tuple(
293
- "hello",
294
- 42,
295
- true,
296
- );
518
+ ### Literal inference
297
519
 
520
+ By default, constructors preserve literal types:
521
+
522
+ ```ts
523
+ const t = tuple("hello", 42, true);
298
524
  // readonly ["hello", 42, true]
525
+
526
+ const r = record({ x: 1, y: 2 });
527
+ // readonly properties x: 1, y: 2
528
+ ```
529
+
530
+ ### Reusable typed factories
531
+
532
+ Use `.like()` to define reusable constructors:
533
+
534
+ ```ts
535
+ const Pair = tuple.like("", 0);
536
+
537
+ const User = record.like({
538
+ id: "",
539
+ name: "",
540
+ });
541
+
542
+ const Point = struct("x", "y").like({
543
+ x: 0,
544
+ y: 0,
545
+ });
299
546
  ```
300
547
 
301
- The library is deliberately small: the runtime API consists of the two constructors you actually need:
548
+ Or specify types explicitly:
302
549
 
303
550
  ```ts
304
- import { tuple, struct } from "tuple-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
+ }>();
305
562
  ```
306
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
+
307
572
  ## When should I use it?
308
573
 
309
574
  `tuple-ts` is particularly useful when you want **small immutable values with stable identity**:
310
575
 
311
- * composite `Map` keys
312
- * `Set` membership
313
- * memoization keys
314
- * graph/search states
315
- * coordinates and other multi-dimensional values
316
- * functional programming
317
- * immutable application state
318
- * 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 |
319
592
 
320
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**.
321
594
 
@@ -323,10 +596,14 @@ If you just need an ordinary mutable array or object, use one. `tuple-ts` is for
323
596
 
324
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.
325
598
 
599
+ Property order is ignored for records and structs, but element order matters for tuples.
600
+
326
601
  The intern cache uses weak references, so canonical values do not need to be kept alive solely because they have previously been created.
327
602
 
603
+ Constructed records and structs are copied before interning, so callers cannot alter an interned value by modifying the source object.
604
+
328
605
  There are no runtime dependencies.
329
606
 
330
607
  ## License
331
608
 
332
- ISC
609
+ ISC
package/dist/index.d.ts CHANGED
@@ -1,6 +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 _structCache: InternCache<number, Record<string, unknown>>;
4
- export declare const tuple: <const T extends readonly unknown[]>(...t: T) => Readonly<T>;
5
- export declare const struct: <const T extends Record<string, unknown>>({ ...r }: T) => Readonly<T>;
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 {};
6
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,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"}
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
@@ -9,21 +9,23 @@ function arrayEquivalent(a1, a2) {
9
9
  }
10
10
  return true;
11
11
  }
12
- function structEquivalent(r1, r2) {
12
+ function recordEquivalent(r1, r2) {
13
13
  const r1Keys = Object.keys(r1);
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
  }
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
+ });
26
25
  export const _tupleCache = new InternCache(hash.valuesHash, arrayEquivalent);
27
- export const _structCache = new InternCache((r) => hash.valuesHash(Object.keys(r).sort().flatMap(k => [k, r[k]])), structEquivalent);
28
- export const tuple = (...t) => Object.freeze(_tupleCache.get(t));
29
- export const struct = ({ ...r }) => Object.freeze(_structCache.get(r));
26
+ export const _recordCache = new InternCache((r) => hash.valuesHash(Object.keys(r)
27
+ .sort()
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,11 +1,11 @@
1
1
  export default class<H, T extends object> {
2
- private readonly map;
3
2
  private readonly hashFn;
4
- private readonly identityFn;
3
+ private readonly matchFn;
4
+ private readonly map;
5
5
  private cleanupRegistry;
6
- constructor(hashFn: (obj: T) => H, identityFn: (obj1: T, obj2: T) => boolean);
6
+ constructor(hashFn: (value: T) => H, matchFn: (a: T, b: T) => boolean);
7
7
  private remove;
8
- get<U extends T>(obj: U): U;
8
+ get<T_ extends T>(obj: T_): Readonly<T_>;
9
9
  get size(): number;
10
10
  reset(): void;
11
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;IACtC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAsC;IAC1D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgB;IACvC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgC;IAC3D,OAAO,CAAC,eAAe,CAErB;gBAGA,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,EACrB,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,KAAK,OAAO;IAM3C,OAAO,CAAC,MAAM;IAWd,GAAG,CAAC,CAAC,SAAS,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC;IAqB3B,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,9 +1,9 @@
1
1
  export default class {
2
- constructor(hashFn, identityFn) {
2
+ constructor(hashFn, matchFn) {
3
+ this.hashFn = hashFn;
4
+ this.matchFn = matchFn;
3
5
  this.map = new Map();
4
6
  this.cleanupRegistry = new FinalizationRegistry((x) => this.remove(x));
5
- this.hashFn = hashFn;
6
- this.identityFn = identityFn;
7
7
  }
8
8
  remove([hash, ref]) {
9
9
  const hashBucket = this.map.get(hash);
@@ -21,7 +21,7 @@ export default class {
21
21
  if (hashBucket !== undefined) {
22
22
  for (const ref of hashBucket) {
23
23
  const o = ref.deref();
24
- if (o && this.identityFn(o, obj))
24
+ if (o && this.matchFn(obj, o))
25
25
  return o;
26
26
  }
27
27
  }
@@ -31,6 +31,7 @@ export default class {
31
31
  this.map.set(hash, hashBucket);
32
32
  }
33
33
  // Object is not in the cache. Add it and return it.
34
+ Object.freeze(obj);
34
35
  const ref = new WeakRef(obj);
35
36
  hashBucket.add(ref);
36
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.4.7",
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",