tuple-ts 0.3.1 → 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 CHANGED
@@ -1,156 +1,330 @@
1
- # TupleTS
1
+ # tuple-ts
2
2
 
3
- TupleTS is an implementation of tuples and records intended to be memory-safe, performant, and tiny.
3
+ **Immutable tuples and structs with value-based identity for JavaScript and TypeScript.**
4
4
 
5
- - **Referential Equality (`===`)**: Identical tuples and records return the same instance in memory.
6
- - **Composite Keys for `Set` & `Map`**: Use tuples or records as keys without custom serialization or hashing workarounds.
7
- - **Memory-Safe**: Uses `WeakRef` and `FinalizationRegistry` to automatically purge entries upon garbage collection.
8
- - **Deeply Nestable & Immutable**: Tuples can contain other tuples; all created tuples and records are `Object.freeze`'d.
9
- - **TypeScript First**: Full type inference and type-safe factories.
10
- - **Zero Dependencies**: Lightweight implementation with fast MurmurHash3-based hashing.
5
+ JavaScript normally compares arrays and objects by reference:
11
6
 
12
- ---
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:
13
14
 
14
- ## Installation
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
15
26
 
16
27
  ```bash
17
28
  npm install tuple-ts
18
29
  ```
19
30
 
20
- ---
31
+ ## Tuples
21
32
 
22
- ## Usage Examples
33
+ Create an immutable tuple with full TypeScript inference:
23
34
 
24
- ### 1. Basic Tuples & Referential Equality
35
+ ```ts
36
+ import { tuple } from "tuple-ts";
25
37
 
26
- Tuples with the same elements evaluate to the exact same object reference (`===`):
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**:
27
46
 
28
47
  ```ts
29
- import { tuple } from "tuple-ts";
48
+ const a = tuple(10, 20);
49
+ const b = tuple(10, 20);
50
+
51
+ a === b; // true
52
+ Object.is(a, b); // true
53
+ ```
54
+
55
+ Different values give you different objects:
30
56
 
31
- const a = tuple(1, "hello", true);
32
- const b = tuple(1, "hello", true);
57
+ ```ts
58
+ tuple(10, 20) === tuple(10, 21); // false
59
+ tuple(10, 20) === tuple(20, 10); // false
60
+ ```
61
+
62
+ Tuples are ordinary frozen arrays, so there is no special API to learn:
33
63
 
34
- console.log(a === b); // true
64
+ ```ts
65
+ const t = tuple("hello", 42, true);
35
66
 
36
- // Tuples are standard frozen arrays:
37
- console.log(a[0]); // 1
38
- console.log(a.length); // 3
39
- console.log([...a]); // [1, "hello", true]
67
+ t.length; // 3
68
+ t[0]; // "hello"
69
+ [...t]; // ["hello", 42, true]
70
+ Object.isFrozen(t); // true
40
71
  ```
41
72
 
42
- ### 2. Nested Tuples
73
+ ## Why is this useful?
74
+
75
+ ### Composite `Map` keys
43
76
 
44
- Tuples can be nested arbitrarily and maintain referential equality across structures:
77
+ JavaScript doesn't have value-based arrays, so this doesn't work:
45
78
 
46
79
  ```ts
47
- import { tuple } from "tuple-ts";
80
+ const map = new Map();
48
81
 
49
- const t1 = tuple("matrix", tuple(1, 0), tuple(0, 1));
50
- const t2 = tuple("matrix", tuple(1, 0), tuple(0, 1));
51
- const t3 = tuple("matrix", tuple(1, 1), tuple(0, 1));
82
+ map.set([10, 20], "visited");
52
83
 
53
- console.log(t1 === t2); // true
54
- console.log(t1 === t3); // false
84
+ map.get([10, 20]); // undefined
55
85
  ```
56
86
 
57
- ### 3. Composite Keys in `Map` and `Set`
58
-
59
- 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`:
60
88
 
61
89
  ```ts
62
- import { tuple } from "tuple-ts";
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.
63
100
 
64
- // Set: Deduplicating coordinate pairs
101
+ ### `Set` of values
102
+
103
+ The same applies to `Set`:
104
+
105
+ ```ts
65
106
  const visited = new Set();
107
+
66
108
  visited.add(tuple(10, 20));
67
109
 
68
- console.log(visited.has(tuple(10, 20))); // true!
69
- console.log(visited.has(tuple(10, 21))); // false
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.
70
115
 
71
- // Map: Multi-argument function memoization / 2D grid
72
- const grid = new Map();
73
- grid.set(tuple(0, 0), "Origin");
74
- grid.set(tuple(3, 4), "Target");
116
+ ## Nested tuples
75
117
 
76
- console.log(grid.get(tuple(0, 0))); // "Origin"
77
- console.log(grid.get(tuple(3, 4))); // "Target"
118
+ Tuples can be nested:
119
+
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
134
+ ```
135
+
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"));
142
+
143
+ a === b; // true
144
+ a === c; // false
78
145
  ```
79
146
 
80
- ### 4. Custom Typed Tuples (`defineTupleType`)
147
+ ## Typed tuples
81
148
 
82
- You can create strictly-typed tuple factories:
149
+ You can create a reusable constructor for a particular tuple type:
83
150
 
84
151
  ```ts
85
- import { defineTupleType } from "tuple-ts";
152
+ type Point = [x: number, y: number];
86
153
 
87
- type Point2D = [x: number, y: number];
88
- const Point = defineTupleType<Point2D>();
154
+ const Point = tuple<Point>;
89
155
 
90
- const p1 = Point(12, 34);
91
- const p2 = Point(12, 34);
156
+ const a = Point(10, 20);
157
+ const b = Point(10, 20);
92
158
 
93
- console.log(p1 === p2); // true
159
+ a === b; // true
94
160
  ```
95
161
 
96
- ### 5. Interned Records (`record`)
162
+ The result retains the tuple's TypeScript type while gaining the same runtime identity semantics.
163
+
164
+ ## Structs
97
165
 
98
- Just like tuples, `record` interns shallow objects. Property key insertion order does not affect equality:
166
+ `struct` provides the same semantics for objects:
99
167
 
100
168
  ```ts
101
- import { record } from "tuple-ts";
169
+ import { struct } from "tuple-ts";
170
+
171
+ const a = struct({
172
+ id: 1,
173
+ role: "admin",
174
+ });
102
175
 
103
- const userA = record({ id: 1, role: "admin" });
104
- const userB = record({ role: "admin", id: 1 }); // different key order
176
+ const b = struct({
177
+ role: "admin",
178
+ id: 1,
179
+ });
105
180
 
106
- console.log(userA === userB); // true
107
- console.log(userA.role); // "admin"
181
+ a === b; // true
108
182
  ```
109
183
 
110
- ### 6. Custom Typed Records (`defineRecordType`)
184
+ Property order does not matter.
185
+
186
+ Structs are frozen:
187
+
188
+ ```ts
189
+ Object.isFrozen(a); // true
190
+ ```
111
191
 
112
- As with tuples, you can also create strictly-typed record factories:
192
+ And they work naturally as `Map` keys:
113
193
 
114
194
  ```ts
115
- import { defineRecordType } from "tuple-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
+ ```
116
206
 
117
- interface UserProfile {
207
+ ### Typed structs
208
+
209
+ As with tuples, you can define a reusable typed constructor:
210
+
211
+ ```ts
212
+ interface User {
118
213
  id: string;
119
214
  name: string;
120
215
  updatedAt: number;
121
216
  }
122
217
 
123
- const User = defineRecordType<UserProfile>();
218
+ const User = struct<User>;
124
219
 
125
- const user1 = User({ id: "alice", name: "Alice", updatedAt: 1000 });
126
- const user2 = User({ name: "Alice", id: "alice", updatedAt: 1000 });
220
+ const a = User({
221
+ id: "alice",
222
+ name: "Alice",
223
+ updatedAt: 1000,
224
+ });
127
225
 
128
- console.log(user1 === user2); // true
129
- console.log(user1["name"]); // "Alice"
226
+ const b = User({
227
+ name: "Alice",
228
+ id: "alice",
229
+ updatedAt: 1000,
230
+ });
231
+
232
+ a === b; // true
130
233
  ```
131
234
 
132
- ### 7. Memory Safety & Garbage Collection
235
+ ## Equality semantics
133
236
 
134
- TupleTS uses `WeakRef` and `FinalizationRegistry` under the hood. When a tuple or record is no longer referenced anywhere in your program, the JavaScript runtime garbage-collects it, and its entry in the intern cache is automatically pruned.
237
+ Primitive values are compared using [`Object.is`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is):
135
238
 
136
239
  ```ts
137
- // No need to manually clear caches or worry about memory leaks.
138
- {
139
- const temp = tuple("transient", 123);
140
- // once 'temp' leaves scope and GC runs, the intern cache entry is freed.
141
- }
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
253
+ ```
254
+
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
142
273
  ```
143
274
 
144
- ---
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.
145
327
 
146
- ## Equality Semantics
328
+ ## License
147
329
 
148
- TupleTS compares primitives via `Object.is`:
149
- - `NaN` is equivalent to `NaN` (`tuple(NaN) === tuple(NaN)`).
150
- - `+0` and `-0` are treated as distinct values (`tuple(+0) !== tuple(-0)`).
151
- - Object elements are interned by object identity:
152
- ```ts
153
- const shared = { value: 1 };
154
- tuple(shared) === tuple(shared); // true
155
- tuple({}) === tuple({}); // false (different object references)
156
- ```
330
+ ISC
package/dist/hash.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export declare function tupleHash(elements: readonly unknown[]): number;
2
- export declare function recordHash(record: Record<string, unknown>): number;
2
+ export declare function structHash(struct: Record<string, unknown>): number;
3
3
  //# sourceMappingURL=hash.d.ts.map
package/dist/hash.js CHANGED
@@ -27,7 +27,7 @@ function linearCongruentialGenerator(seed = Date.now()) {
27
27
  }
28
28
  const randomUint32 = linearCongruentialGenerator();
29
29
  const tupleSeed = randomUint32();
30
- const recordSeed = randomUint32();
30
+ const structSeed = randomUint32();
31
31
  const nullHash = randomUint32();
32
32
  const undefinedHash = randomUint32();
33
33
  const falseHash = randomUint32();
@@ -102,12 +102,12 @@ function valueHashes(values) {
102
102
  export function tupleHash(elements) {
103
103
  return mixHashes(valueHashes(elements), tupleSeed);
104
104
  }
105
- function keyAndValueHashes(record) {
106
- return Object.keys(record).sort().flatMap(key => [
105
+ function keyAndValueHashes(struct) {
106
+ return Object.keys(struct).sort().flatMap(key => [
107
107
  valueHash(key),
108
- valueHash(record[key])
108
+ valueHash(struct[key])
109
109
  ]);
110
110
  }
111
- export function recordHash(record) {
112
- return mixHashes(keyAndValueHashes(record), recordSeed);
111
+ export function structHash(struct) {
112
+ return mixHashes(keyAndValueHashes(struct), structSeed);
113
113
  }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  import InternCache from "./intern-cache";
2
2
  export { InternCache };
3
- export type TupleType<T extends readonly unknown[]> = (...t: T) => Readonly<T>;
4
- export type RecordType<T extends Record<string, unknown>> = (r: T) => Readonly<T>;
5
3
  export declare const _tupleCache: InternCache<number, readonly unknown[]>;
6
- export declare const _recordCache: InternCache<number, Record<string, unknown>>;
7
- export declare const tuple: <const T_ extends readonly unknown[]>(...t: T_) => Readonly<T_>;
8
- export declare const record: <const T_ extends Record<string, unknown>>({ ...r }: T_) => T_;
9
- export declare const defineTupleType: <T extends readonly unknown[]>() => TupleType<T>;
10
- export declare const defineRecordType: <T extends Record<string, unknown>>() => RecordType<T>;
4
+ export declare const _structCache: InternCache<number, Record<string, unknown>>;
5
+ export declare const tuple: <const T extends readonly unknown[]>(...t: T) => Readonly<T>;
6
+ export declare const struct: <const T extends Record<string, unknown>>({ ...r }: T) => Readonly<T>;
11
7
  //# 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;AACzC,OAAO,EAAE,WAAW,EAAE,CAAC;AAGvB,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC;AAC/E,MAAM,MAAM,UAAU,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAC1D,CAAC,EAAE,CAAC,KACD,QAAQ,CAAC,CAAC,CAAC,CAAC;AAEjB,eAAO,MAAM,WAAW,yCAGvB,CAAC;AACF,eAAO,MAAM,YAAY,8CAGxB,CAAC;AA4BF,eAAO,MAAM,KAAK,GAAI,KAAK,CAAC,EAAE,SAAS,SAAS,OAAO,EAAE,EAAE,GAAG,GAAG,EAAE,iBAChC,CAAC;AAEpC,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAE9D,EAAE,KAAG,EAAwC,CAAC;AAEjD,eAAO,MAAM,eAAe,GACzB,CAAC,SAAS,SAAS,OAAO,EAAE,OAAK,SAAS,CAAC,CAAC,CAEjB,CAAC;AAE/B,eAAO,MAAM,gBAAgB,GAC1B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,OAAK,UAAU,CAAC,CAAC,CAExC,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 _recordCache = new InternCache(hash.recordHash, recordEquivalent);
6
4
  function arrayEquivalent(a1, a2) {
7
5
  if (a2.length !== a1.length)
8
6
  return false;
@@ -12,7 +10,7 @@ function arrayEquivalent(a1, a2) {
12
10
  }
13
11
  return true;
14
12
  }
15
- function recordEquivalent(r1, r2) {
13
+ function structEquivalent(r1, r2) {
16
14
  const r1Keys = Object.keys(r1);
17
15
  const r2Keys = Object.keys(r2);
18
16
  if (r2Keys.length !== r1Keys.length)
@@ -26,7 +24,7 @@ function recordEquivalent(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
- export const record = ({ ...r }) => Object.freeze(_recordCache.get(r));
31
- export const defineTupleType = () => (...t) => tuple(...t);
32
- export const defineRecordType = () => (r) => record(r);
30
+ export const struct = ({ ...r }) => Object.freeze(_structCache.get(r));
@@ -1,18 +1,24 @@
1
- import { record } from ".";
2
- describe("record", () => {
1
+ import { struct } from ".";
2
+ describe("struct", () => {
3
3
  it("preserves keys and values", () => {
4
- const a = record({ x: 1, y: 2 });
4
+ const a = struct({ x: 1, y: 2 });
5
5
  expect(a).toEqual({ x: 1, y: 2 });
6
6
  });
7
7
  it("returns the same object for shallow-equal input objects", () => {
8
- const a = record({ x: 1, y: 2 });
9
- const b = record({ x: 1, y: 2 });
8
+ const a = struct({ x: 1, y: 2 });
9
+ const b = struct({ x: 1, y: 2 });
10
10
  expect(a).toBe(b);
11
11
  });
12
12
  it("returns different object if keys or values differ", () => {
13
- const r = record({ x: 1, y: 2 });
14
- expect(record({ x: 1, y: 3 })).not.toBe(r);
15
- expect(record({ x: 1, y: 3 })).not.toBe(r);
16
- expect(record({ x: 1, y: 2, z: 3 })).not.toBe(r);
13
+ const r = struct({ x: 1, y: 2 });
14
+ expect(struct({ x: 1, y: 3 })).not.toBe(r);
15
+ expect(struct({ x: 1, y: 3 })).not.toBe(r);
16
+ expect(struct({ x: 1, y: 2, z: 3 })).not.toBe(r);
17
+ });
18
+ it("can be customised", () => {
19
+ const geocode = (struct);
20
+ const a = geocode({ lat: 1, long: 2 });
21
+ const b = geocode({ lat: 1, long: 2 });
22
+ expect(a).toBe(b);
17
23
  });
18
24
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=struct.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"struct.spec.d.ts","sourceRoot":"","sources":["../src/struct.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,35 @@
1
+ import { struct } from ".";
2
+ describe("struct", () => {
3
+ it("preserves keys and values", () => {
4
+ const obj = { x: 1, y: 2 };
5
+ const s = struct(obj);
6
+ expect(s).toEqual(obj);
7
+ });
8
+ it("returns the same object for shallow-equal input objects", () => {
9
+ const a = struct({ x: 1, y: 2 });
10
+ const b = struct({ x: 1, y: 2 });
11
+ expect(a).toBe(b);
12
+ expect(a === b).toBe(true);
13
+ });
14
+ it("returns different object if keys or values differ", () => {
15
+ const s1 = struct({ x: 1, y: 2 });
16
+ const s2 = struct({ x: 1, y: 3 });
17
+ const s3 = struct({ x: 1, y: 2, z: 3 });
18
+ expect(s1).not.toBe(s2);
19
+ expect(s1).not.toBe(s3);
20
+ expect(s1 === s3).toBe(false);
21
+ });
22
+ it("can be customised", () => {
23
+ const geocode = (struct);
24
+ const a = geocode({ lat: 1, long: 2 });
25
+ const b = geocode({ lat: 1, long: 2 });
26
+ expect(a).toBe(b);
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
+ });
35
+ });
@@ -1,4 +1,4 @@
1
- import { tuple, defineTupleType, _tupleCache } from ".";
1
+ import { tuple, _tupleCache } from ".";
2
2
  async function waitForGC() {
3
3
  await new Promise((res) => setTimeout(res, 0));
4
4
  global.gc(); // requires node --expose-gc
@@ -7,7 +7,8 @@ async function waitForGC() {
7
7
  test("Empty tuples", () => {
8
8
  const a = tuple();
9
9
  const b = tuple();
10
- expect(b).toBe(a);
10
+ expect(b).toBe(a); // identity
11
+ expect(a === b).toBe(true); // equality
11
12
  });
12
13
  test("Indexability", () => {
13
14
  const a = {};
@@ -27,12 +28,14 @@ test("Basic interned equality (if same input, same object)", () => {
27
28
  const f = () => { };
28
29
  const t1 = tuple(a, f, 42, "hi", undefined, true, null, 7839278492n);
29
30
  const t2 = tuple(a, f, 42, "hi", undefined, true, null, 7839278492n);
30
- expect(t1).toBe(t2); // same object reference
31
+ expect(t1).toBe(t2); // identity
32
+ expect(t1 === t2).toBe(true); // equality
31
33
  });
32
34
  test("Referential inequality (same structure, different references)", () => {
33
35
  const t1 = tuple({}, 42);
34
36
  const t2 = tuple({}, 42);
35
- expect(t1).not.toBe(t2); // different objects
37
+ expect(t1).not.toBe(t2); // non-identity
38
+ expect(t1 === t2).toBe(false); // non-equality
36
39
  });
37
40
  test("Different-length tuples are different tuples", () => {
38
41
  const t1 = tuple(1, 2);
@@ -46,10 +49,12 @@ test("Nestable", () => {
46
49
  const t2 = tuple("a", tuple("b1", "b2"), "c");
47
50
  const t3 = tuple("a", tuple("b1", "b3"), "c");
48
51
  expect(t2).toBe(t1);
52
+ expect(t1 === t2).toBe(true);
49
53
  expect(t3).not.toBe(t1);
50
54
  });
51
55
  test("NaN is equivalent to itself", () => {
52
56
  expect(tuple(NaN)).toBe(tuple(NaN));
57
+ expect(tuple(NaN) === tuple(NaN)).toBe(true);
53
58
  });
54
59
  test("NaN values with different bit representations are interned as identical", () => {
55
60
  // Construct NaN with payload 1
@@ -69,16 +74,29 @@ test("NaN values with different bit representations are interned as identical",
69
74
  // If NaNs were not canonicalized in numberToUint32s, this would fail (.not.toBe)
70
75
  // because nan1 and nan2 would hash to different buckets:
71
76
  expect(tuple(nan1)).toBe(tuple(nan2));
77
+ expect(tuple(nan1) === tuple(nan2)).toBe(true);
72
78
  });
73
79
  test("Positive and negative zero are distinct", () => {
80
+ const t1 = tuple(+0);
81
+ const t2 = tuple(-0);
82
+ expect(t1).not.toBe(t2);
83
+ expect(t1 === t2).toBe(false);
74
84
  expect(tuple(+0)).not.toBe(tuple(-0));
85
+ expect(tuple(+0) === tuple(-0)).toBe(false);
75
86
  });
76
87
  test("Tuple cache entry is cleared when tuple is garbage-collected", async () => {
77
88
  _tupleCache.reset();
78
89
  expect(_tupleCache.size).toBe(0);
79
- const Point = defineTupleType();
90
+ const Point = (tuple);
80
91
  Point(15, 42);
81
92
  expect(_tupleCache.size).toBe(1);
82
93
  await waitForGC();
83
94
  expect(_tupleCache.size).toBe(0);
84
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
+ });
package/package.json CHANGED
@@ -1,21 +1,14 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "tuple-ts",
4
- "version": "0.3.1",
5
- "description": "Python-like tuples for JavaScript/TypeScript",
4
+ "version": "0.4.1",
5
+ "description": "Tuples and structs for TypeScript and JavaScript with equality and identity semantics.",
6
6
  "keywords": [
7
7
  "tuple",
8
- "tuples",
9
- "record",
10
- "records",
11
- "interning",
12
- "hash-consing",
13
- "value-equality",
14
- "structural-equality",
15
- "composite-key",
16
- "immutable",
17
- "immutability",
18
- "weakref",
8
+ "struct",
9
+ "equality",
10
+ "identity",
11
+ "shallow-equality",
19
12
  "typescript"
20
13
  ],
21
14
  "main": "./dist/index.js",