ts-data-forge 14.0.0 → 14.0.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.
Files changed (34) hide show
  1. package/README.md +3 -2
  2. package/dist/array/impl/array-utils-modification.d.mts +18 -5
  3. package/dist/array/impl/array-utils-modification.d.mts.map +1 -1
  4. package/dist/array/impl/array-utils-modification.mjs.map +1 -1
  5. package/dist/array/impl/array-utils-slice-clamped.d.mts.map +1 -1
  6. package/dist/array/impl/array-utils-slice-clamped.mjs.map +1 -1
  7. package/dist/array/impl/array-utils-transformation.d.mts +8 -3
  8. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  9. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  10. package/dist/collections/imap.d.mts +1 -1
  11. package/dist/collections/imap.mjs.map +1 -1
  12. package/dist/collections/iset-mapped.d.mts +2 -2
  13. package/dist/collections/iset-mapped.d.mts.map +1 -1
  14. package/dist/collections/iset-mapped.mjs.map +1 -1
  15. package/dist/collections/iset.d.mts +2 -2
  16. package/dist/collections/iset.d.mts.map +1 -1
  17. package/dist/collections/iset.mjs.map +1 -1
  18. package/dist/number/num-arithmetic-types.d.mts +2 -2
  19. package/dist/number/num-arithmetic-types.d.mts.map +1 -1
  20. package/package.json +4 -5
  21. package/src/array/impl/array-utils-creation.test.mts +10 -8
  22. package/src/array/impl/array-utils-length-constraint-preservation.test.mts +2 -2
  23. package/src/array/impl/array-utils-modification.mts +41 -33
  24. package/src/array/impl/array-utils-modification.test.mts +52 -0
  25. package/src/array/impl/array-utils-shape-invariants.test.mts +284 -0
  26. package/src/array/impl/array-utils-slice-clamped.mts +2 -1
  27. package/src/array/impl/array-utils-transformation.mts +29 -2
  28. package/src/array/impl/array-utils-transformation.test.mts +34 -7
  29. package/src/collections/imap.mts +1 -1
  30. package/src/collections/iset-mapped.mts +7 -3
  31. package/src/collections/iset.mts +7 -3
  32. package/src/collections/iset.test.mts +9 -6
  33. package/src/number/num-arithmetic-types.mts +8 -4
  34. package/src/number/num.test.mts +2 -2
@@ -0,0 +1,284 @@
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthArray,
4
+ type FixedLengthTuple,
5
+ type MaxLengthArray,
6
+ type MaxLengthOf,
7
+ type MinLengthArray,
8
+ type MinLengthOf,
9
+ } from 'ts-type-forge';
10
+ import { expectType } from '../../expect-type.mjs';
11
+ import {
12
+ asBoundedLengthArray,
13
+ asFixedLengthArray,
14
+ asMaxLengthArray,
15
+ asMinLengthArray,
16
+ } from './array-utils-length-bounded-array-cast.mjs';
17
+ import { toFilled, toRangeFilled } from './array-utils-modification.mjs';
18
+ import { map, toReversed, toSorted } from './array-utils-transformation.mjs';
19
+
20
+ /**
21
+ * Invariant-based coverage for the array -> array transforms.
22
+ *
23
+ * Rather than pinning an expected type for every (brand x shape x context)
24
+ * combination — which is a large table whose expected side is easy to derive
25
+ * from the implementation and therefore prove nothing — this asserts the small
26
+ * set of properties every shape-preserving transform must satisfy, across a
27
+ * fixed list of sample inputs that covers the axes.
28
+ *
29
+ * The properties are:
30
+ *
31
+ * 1. **Length** — the result reports the same `length` as the input.
32
+ * 2. **Bounds** — a length brand on the input survives onto the result.
33
+ * 3. **Generic context** — from a caller that is itself generic over the
34
+ * array, the result is assignable to the caller's own homomorphic mapping.
35
+ *
36
+ * Property 3 is the one with no coverage before this file, and is what the
37
+ * `Arr.map` regression fixed in this PR violated: a conditional return type a
38
+ * generic `Ar` cannot decide stays deferred, and its brand-carrying branch is
39
+ * then not assignable to the caller's mapping.
40
+ */
41
+
42
+ // The sample inputs. Between them they cover the axes: empty / singleton /
43
+ // fixed tuple / non-empty tuple / unbounded array on the shape axis, and
44
+ // unbranded / lower bound / upper bound / both / exact on the brand axis.
45
+ const emptyTuple: readonly [] = [] as const;
46
+
47
+ const singleton: readonly [1] = [1] as const;
48
+
49
+ const fixedTuple: readonly [1, 2, 3] = [1, 2, 3] as const;
50
+
51
+ const nonEmptyTuple: readonly [1, ...number[]] = [1, 2, 3] as const;
52
+
53
+ const plainArray: readonly number[] = [1, 2, 3] as const;
54
+
55
+ const lowerBounded: MinLengthArray<2, number> = asMinLengthArray(2, [1, 2, 3]);
56
+
57
+ const upperBounded: MaxLengthArray<5, number> = asMaxLengthArray(5, [1, 2, 3]);
58
+
59
+ const bounded: BoundedLengthArray<2, 5, number> = asBoundedLengthArray(
60
+ 2,
61
+ 5,
62
+ [1, 2, 3],
63
+ );
64
+
65
+ const exactLength: FixedLengthArray<3, number> = asFixedLengthArray(
66
+ 3,
67
+ [1, 2, 3],
68
+ );
69
+
70
+ /**
71
+ * Property 3 — generic context.
72
+ *
73
+ * Each of these is an assertion by construction: the annotated return type is
74
+ * the caller's own homomorphic mapping, so the declaration only compiles while
75
+ * the transform reports something assignable to it. There is nothing to run.
76
+ *
77
+ * Only the transforms that satisfy this today are listed. `toSorted`,
78
+ * `toSortedBy`, `toReversed`, `toUpdated`, `tail`, `butLast` and `zip` do not
79
+ * yet — see the PR description for why each one resists the same fix. Add them
80
+ * here as they are fixed; that is what makes this list the coverage record.
81
+ */
82
+
83
+ const mapInGenericContext = <const T extends readonly number[]>(
84
+ xs: T,
85
+ ): Readonly<{ [K in keyof T]: string }> => map(xs, String);
86
+
87
+ const mapCurriedInGenericContext = <const T extends readonly number[]>(
88
+ xs: T,
89
+ ): Readonly<{ [K in keyof T]: string }> => map(String)(xs);
90
+
91
+ const toFilledInGenericContext = <const T extends readonly number[]>(
92
+ xs: T,
93
+ ): Readonly<{ [K in keyof T]: 0 }> => toFilled(xs, 0);
94
+
95
+ const toFilledCurriedInGenericContext = <const T extends readonly number[]>(
96
+ xs: T,
97
+ ): Readonly<{ [K in keyof T]: 0 }> => toFilled(0)(xs);
98
+
99
+ const toRangeFilledInGenericContext = <const T extends readonly number[]>(
100
+ xs: T,
101
+ ): Readonly<{ [K in keyof T]: number }> => toRangeFilled(xs, 0, [0, 1]);
102
+
103
+ describe('Arr shape invariants', () => {
104
+ // The type-level assertions below are checked by `tsc`; this pins the same
105
+ // property at runtime, so a transform that silently drops or adds an element
106
+ // fails here even where the type says otherwise.
107
+ test('every shape-preserving transform keeps the length at runtime', () => {
108
+ const input = [3, 1, 2] as const;
109
+
110
+ assert.deepStrictEqual<FixedLengthTuple<5, 3>>(
111
+ [
112
+ map(input, String).length,
113
+ toFilled(input, 0).length,
114
+ toRangeFilled(input, 0, [1, 2]).length,
115
+ toSorted(input).length,
116
+ toReversed(input).length,
117
+ ] as const,
118
+ [3, 3, 3, 3, 3] as const,
119
+ );
120
+ });
121
+
122
+ test('the generic-context callers agree with their annotations', () => {
123
+ const input = [3, 1, 2] as const;
124
+
125
+ assert.deepStrictEqual<FixedLengthTuple<3, string>>(
126
+ mapInGenericContext(input),
127
+ ['3', '1', '2'],
128
+ );
129
+
130
+ assert.deepStrictEqual<FixedLengthTuple<3, string>>(
131
+ mapCurriedInGenericContext(input),
132
+ ['3', '1', '2'],
133
+ );
134
+
135
+ assert.deepStrictEqual<FixedLengthTuple<3, 0>>(
136
+ toFilledInGenericContext(input),
137
+ [0, 0, 0],
138
+ );
139
+
140
+ assert.deepStrictEqual<FixedLengthTuple<3, 0>>(
141
+ toFilledCurriedInGenericContext(input),
142
+ [0, 0, 0],
143
+ );
144
+
145
+ assert.deepStrictEqual<FixedLengthTuple<3, number>>(
146
+ toRangeFilledInGenericContext(input),
147
+ [0, 1, 2],
148
+ );
149
+ });
150
+ });
151
+
152
+ // `map` — property 1 (length) across every unbranded sample.
153
+ {
154
+ const _a = map(emptyTuple, String);
155
+
156
+ const _b = map(singleton, String);
157
+
158
+ const _c = map(fixedTuple, String);
159
+
160
+ const _d = map(nonEmptyTuple, String);
161
+
162
+ const _e = map(plainArray, String);
163
+
164
+ expectType<(typeof _a)['length'], (typeof emptyTuple)['length']>('=');
165
+
166
+ expectType<(typeof _b)['length'], (typeof singleton)['length']>('=');
167
+
168
+ expectType<(typeof _c)['length'], (typeof fixedTuple)['length']>('=');
169
+
170
+ expectType<(typeof _d)['length'], (typeof nonEmptyTuple)['length']>('=');
171
+
172
+ expectType<(typeof _e)['length'], (typeof plainArray)['length']>('=');
173
+ }
174
+
175
+ // `map` — property 2 (bounds) across every branded sample.
176
+ {
177
+ const _a = map(lowerBounded, String);
178
+
179
+ const _b = map(upperBounded, String);
180
+
181
+ const _c = map(bounded, String);
182
+
183
+ const _d = map(exactLength, String);
184
+
185
+ expectType<MinLengthOf<typeof _a>, MinLengthOf<typeof lowerBounded>>('=');
186
+
187
+ expectType<MaxLengthOf<typeof _b>, MaxLengthOf<typeof upperBounded>>('=');
188
+
189
+ expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof bounded>>('=');
190
+
191
+ expectType<MaxLengthOf<typeof _c>, MaxLengthOf<typeof bounded>>('=');
192
+
193
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof exactLength>>('=');
194
+
195
+ expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof exactLength>>('=');
196
+ }
197
+
198
+ // `toFilled` — properties 1 and 2.
199
+ {
200
+ const _a = toFilled(fixedTuple, 0);
201
+
202
+ const _b = toFilled(nonEmptyTuple, 0);
203
+
204
+ const _c = toFilled(plainArray, 0);
205
+
206
+ const _d = toFilled(lowerBounded, 0);
207
+
208
+ const _e = toFilled(bounded, 0);
209
+
210
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
211
+
212
+ expectType<(typeof _b)['length'], (typeof nonEmptyTuple)['length']>('=');
213
+
214
+ expectType<(typeof _c)['length'], (typeof plainArray)['length']>('=');
215
+
216
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof lowerBounded>>('=');
217
+
218
+ expectType<MinLengthOf<typeof _e>, MinLengthOf<typeof bounded>>('=');
219
+
220
+ expectType<MaxLengthOf<typeof _e>, MaxLengthOf<typeof bounded>>('=');
221
+ }
222
+
223
+ // `toRangeFilled` — properties 1 and 2.
224
+ {
225
+ const _a = toRangeFilled(fixedTuple, 0, [1, 2]);
226
+
227
+ const _b = toRangeFilled(plainArray, 0, [0, 1]);
228
+
229
+ const _c = toRangeFilled(lowerBounded, 0, [0, 1]);
230
+
231
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
232
+
233
+ expectType<(typeof _b)['length'], (typeof plainArray)['length']>('=');
234
+
235
+ expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
236
+ }
237
+
238
+ // `toSorted` — properties 1 and 2. Sorting neither adds nor drops elements, so
239
+ // it is held to the same contract even though it is not fixed for property 3.
240
+ {
241
+ const _a = toSorted(fixedTuple);
242
+
243
+ const _b = toSorted(nonEmptyTuple);
244
+
245
+ const _c = toSorted(plainArray);
246
+
247
+ const _d = toSorted(lowerBounded);
248
+
249
+ const _e = toSorted(bounded);
250
+
251
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
252
+
253
+ expectType<(typeof _b)['length'], (typeof nonEmptyTuple)['length']>('=');
254
+
255
+ expectType<(typeof _c)['length'], (typeof plainArray)['length']>('=');
256
+
257
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof lowerBounded>>('=');
258
+
259
+ expectType<MinLengthOf<typeof _e>, MinLengthOf<typeof bounded>>('=');
260
+
261
+ expectType<MaxLengthOf<typeof _e>, MaxLengthOf<typeof bounded>>('=');
262
+ }
263
+
264
+ // `toReversed` — properties 1 and 2. Reversal permutes, so the length and the
265
+ // bounds are exactly what must not move.
266
+ {
267
+ const _a = toReversed(fixedTuple);
268
+
269
+ const _b = toReversed(plainArray);
270
+
271
+ const _c = toReversed(lowerBounded);
272
+
273
+ const _d = toReversed(bounded);
274
+
275
+ expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
276
+
277
+ expectType<(typeof _b)['length'], (typeof plainArray)['length']>('=');
278
+
279
+ expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
280
+
281
+ expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof bounded>>('=');
282
+
283
+ expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof bounded>>('=');
284
+ }
@@ -1,3 +1,4 @@
1
+ import { type FixedLengthTuple } from 'ts-type-forge';
1
2
  import { Num } from '../../number/index.mjs';
2
3
  import { type ArgArrayIndexWithNegative, type SizeType } from '../../types.mjs';
3
4
 
@@ -36,7 +37,7 @@ export function sliceClamped<E>(
36
37
  SizeType.ArgArrWithNegative,
37
38
  SizeType.ArgArrWithNegative,
38
39
  ]
39
- | readonly [SizeType.ArgArrWithNegative, SizeType.ArgArrWithNegative]
40
+ | FixedLengthTuple<2, SizeType.ArgArrWithNegative>
40
41
  ): readonly E[] | ((array: readonly E[]) => readonly E[]) {
41
42
  switch (args.length) {
42
43
  case 3:
@@ -12,6 +12,7 @@ import {
12
12
  type Primitive,
13
13
  type SafeUintWithSmallInt,
14
14
  type SupportedLength,
15
+ type UnknownBrand,
15
16
  type WithSmallInt,
16
17
  } from 'ts-type-forge';
17
18
  import { IMap } from '../../collections/index.mjs';
@@ -46,15 +47,41 @@ import { size } from './array-utils-size.mjs';
46
47
  * assert.deepStrictEqual(indexed, ['0:1', '1:2', '2:3']);
47
48
  * ```
48
49
  */
49
- export function map<const Ar extends readonly unknown[], B>(
50
+ export function map<const Ar extends readonly unknown[] & UnknownBrand, B>(
50
51
  array: Ar,
51
52
  mapFn: (a: Ar[number], index: ArrayIndex<Ar>) => B,
52
53
  ): ChangeArrayElement<Ar, B>;
53
54
 
55
+ // A plain array or tuple carries no length brand, so the homomorphic mapping
56
+ // is the whole answer. Stating it directly — rather than going through
57
+ // `ChangeArrayElement`, whose `HasLengthConstraint` test a *generic* `Ar`
58
+ // cannot decide — is what keeps `map` usable inside a function that is itself
59
+ // generic over the tuple: a deferred conditional is not assignable to the
60
+ // caller's own `{ [K in keyof Ar]: B }`, while this is.
61
+ export function map<const Ar extends readonly unknown[], B>(
62
+ array: Ar,
63
+ mapFn: (a: Ar[number], index: ArrayIndex<Ar>) => B,
64
+ ): Readonly<{ [K in keyof Ar]: B }>;
65
+
54
66
  // curried version
67
+ //
68
+ // The two cases have to be overloads *of the returned function*, not two
69
+ // overloads of `map` itself: both curried signatures take the same single
70
+ // argument, so overload resolution would always pick the first and the second
71
+ // would be dead.
72
+ //
73
+ // Spelled as an intersection of two function types rather than one object type
74
+ // carrying two call signatures — the `convert-to-readonly` codemod wraps an
75
+ // inline object type in `Readonly<…>`, and a mapped type drops call
76
+ // signatures, which would leave the curried form with none at all.
55
77
  export function map<A, B>(
56
78
  mapFn: (a: A, index: SizeType.Arr) => B,
57
- ): <const Ar extends readonly A[]>(array: Ar) => ChangeArrayElement<Ar, B>;
79
+ ): (<const Ar extends readonly A[] & UnknownBrand>(
80
+ array: Ar,
81
+ ) => ChangeArrayElement<Ar, B>) &
82
+ (<const Ar extends readonly A[]>(
83
+ array: Ar,
84
+ ) => Readonly<{ [K in keyof Ar]: B }>);
58
85
 
59
86
  export function map<A, B>(
60
87
  ...args:
@@ -3,11 +3,13 @@ import {
3
3
  type FixedLengthTuple,
4
4
  type MinLengthArray,
5
5
  type NonEmptyArray,
6
+ type NonEmptyTuple,
6
7
  } from 'ts-type-forge';
7
8
  import { IMap } from '../../collections/index.mjs';
8
9
  import { expectType } from '../../expect-type.mjs';
9
10
  import { Optional } from '../../functional/index.mjs';
10
11
  import { SafeUint } from '../../number/index.mjs';
12
+ import { asMinLengthArray } from './array-utils-length-bounded-array-cast.mjs';
11
13
  import {
12
14
  cartesianProduct,
13
15
  concat,
@@ -52,7 +54,7 @@ describe('Arr transformations', () => {
52
54
 
53
55
  const mappedMixed = map(mixed, (x) => typeof x);
54
56
 
55
- expectType<typeof mappedMixed, readonly [string, string, string]>('<=');
57
+ expectType<typeof mappedMixed, FixedLengthTuple<3, string>>('<=');
56
58
 
57
59
  assert.deepStrictEqual(mappedMixed, ['number', 'string', 'boolean']);
58
60
  });
@@ -62,12 +64,37 @@ describe('Arr transformations', () => {
62
64
 
63
65
  const mappedWithIndex = map(tuple, (x, i) => `${i}:${x}`);
64
66
 
65
- expectType<typeof mappedWithIndex, readonly [string, string, string]>(
66
- '<=',
67
- );
67
+ expectType<typeof mappedWithIndex, FixedLengthTuple<3, string>>('<=');
68
68
 
69
69
  assert.deepStrictEqual(mappedWithIndex, ['0:a', '1:b', '2:c']);
70
70
  });
71
+
72
+ test('should preserve the tuple shape under a generic parameter', () => {
73
+ // A caller that is itself generic over the tuple cannot resolve a
74
+ // conditional return type, so `map` has to report the homomorphic
75
+ // mapping directly for a parameter that carries no length brand.
76
+ const mapValues = <const T extends readonly Readonly<{ v: unknown }>[]>(
77
+ boxes: T,
78
+ ): Readonly<{ [K in keyof T]: unknown }> => map(boxes, (b) => b.v);
79
+
80
+ const result = mapValues([{ v: 1 }, { v: 'a' }] as const);
81
+
82
+ expectType<typeof result, FixedLengthTuple<2, unknown>>('=');
83
+
84
+ assert.deepStrictEqual<FixedLengthTuple<2, unknown>>(result, [1, 'a']);
85
+ });
86
+
87
+ test('should keep the length brand of a branded array', () => {
88
+ const lower: MinLengthArray<2, number> = asMinLengthArray(2, [1, 2, 3]);
89
+
90
+ const mappedLower = map(lower, String);
91
+
92
+ expectType<typeof mappedLower, MinLengthArray<2, string>>('~=');
93
+
94
+ // The brand is not part of the runtime value, so compare structurally —
95
+ // the type argument is what widens it, no intermediate binding needed.
96
+ assert.deepStrictEqual<readonly string[]>(mappedLower, ['1', '2', '3']);
97
+ });
71
98
  });
72
99
 
73
100
  describe(scan, () => {
@@ -76,7 +103,7 @@ describe('Arr transformations', () => {
76
103
 
77
104
  const runningSum = scan(numbers, (acc, curr) => acc + curr, 0);
78
105
 
79
- expectType<typeof runningSum, readonly [number, ...number[]]>('<=');
106
+ expectType<typeof runningSum, NonEmptyTuple<number>>('<=');
80
107
 
81
108
  assert.deepStrictEqual<readonly number[]>(runningSum, [0, 1, 3, 6, 10]);
82
109
  });
@@ -134,7 +161,7 @@ describe('Arr transformations', () => {
134
161
 
135
162
  const result = scan(strings, (acc, curr) => acc + curr.length, 0);
136
163
 
137
- expectType<typeof result, readonly [number, ...number[]]>('<=');
164
+ expectType<typeof result, NonEmptyTuple<number>>('<=');
138
165
 
139
166
  assert.deepStrictEqual<readonly number[]>(result, [0, 1, 2, 3]);
140
167
  });
@@ -1015,7 +1042,7 @@ describe('Arr transformations', () => {
1015
1042
 
1016
1043
  const zipped = zip(xs, ys);
1017
1044
 
1018
- expectType<typeof zipped, readonly (readonly [number, number])[]>('=');
1045
+ expectType<typeof zipped, readonly FixedLengthTuple<2, number>[]>('=');
1019
1046
 
1020
1047
  test('case 2', () => {
1021
1048
  assert.deepStrictEqual(zipped, [[1, 4]]);
@@ -549,7 +549,7 @@ type IMapInterface<K extends MapSetKeyType, V> = Readonly<{
549
549
  * const entries = [
550
550
  * ['k1', 'v1'],
551
551
  * ['k2', 'v2'],
552
- * ] as const satisfies readonly (readonly [string, string])[];
552
+ * ] as const satisfies readonly FixedLengthTuple<2, string>[];
553
553
  *
554
554
  * const map = IMap.create(entries);
555
555
  *
@@ -1,5 +1,9 @@
1
1
  /* eslint-disable unicorn/prefer-iterator-to-array-at-end */
2
- import { type ReadonlyRecord, type WidenLiteral } from 'ts-type-forge';
2
+ import {
3
+ type FixedLengthTuple,
4
+ type ReadonlyRecord,
5
+ type WidenLiteral,
6
+ } from 'ts-type-forge';
3
7
  import { asUint32 } from '../number/index.mjs';
4
8
  import { type MapSetKeyType, type SizeType } from '../types.mjs';
5
9
 
@@ -788,7 +792,7 @@ type ISetMappedInterface<K, KM extends MapSetKeyType> = Readonly<{
788
792
  *
789
793
  * @returns An iterable iterator of entries.
790
794
  */
791
- entries: () => IterableIterator<readonly [K, K]>;
795
+ entries: () => IterableIterator<FixedLengthTuple<2, K>>;
792
796
 
793
797
  // Conversion
794
798
 
@@ -1444,7 +1448,7 @@ class ISetMappedClass<K, KM extends MapSetKeyType>
1444
1448
  }
1445
1449
 
1446
1450
  /** @inheritdoc */
1447
- *entries(): IterableIterator<readonly [K, K]> {
1451
+ *entries(): IterableIterator<FixedLengthTuple<2, K>> {
1448
1452
  for (const km of this.#set) {
1449
1453
  // JavaScript Set's entries() yields [value, value]
1450
1454
  const a = this.#fromKey(km);
@@ -1,5 +1,9 @@
1
1
  /* eslint-disable unicorn/prefer-iterator-to-array-at-end */
2
- import { type ReadonlyRecord, type WidenLiteral } from 'ts-type-forge';
2
+ import {
3
+ type FixedLengthTuple,
4
+ type ReadonlyRecord,
5
+ type WidenLiteral,
6
+ } from 'ts-type-forge';
3
7
  import { asUint32 } from '../number/index.mjs';
4
8
  import { unknownToString } from '../others/index.mjs';
5
9
  import { type MapSetKeyType, type SizeType } from '../types.mjs';
@@ -460,7 +464,7 @@ type ISetInterface<K extends MapSetKeyType> = Readonly<{
460
464
  *
461
465
  * @returns An iterable iterator of entries.
462
466
  */
463
- entries: () => IterableIterator<readonly [K, K]>;
467
+ entries: () => IterableIterator<FixedLengthTuple<2, K>>;
464
468
 
465
469
  // Conversion
466
470
 
@@ -913,7 +917,7 @@ class ISetClass<K extends MapSetKeyType> implements ISet<K>, Iterable<K> {
913
917
  }
914
918
 
915
919
  /** @inheritdoc */
916
- entries(): IterableIterator<readonly [K, K]> {
920
+ entries(): IterableIterator<FixedLengthTuple<2, K>> {
917
921
  return this.#set.entries();
918
922
  }
919
923
 
@@ -1,4 +1,9 @@
1
- import { type DeepReadonly, type Primitive } from 'ts-type-forge';
1
+ import {
2
+ type DeepReadonly,
3
+ type FixedLengthTuple,
4
+ type MutableFixedLengthTuple,
5
+ type Primitive,
6
+ } from 'ts-type-forge';
2
7
  import { expectType } from '../expect-type.mjs';
3
8
  import { isString } from '../guard/index.mjs';
4
9
  import { type MapSetKeyType } from '../types.mjs';
@@ -974,13 +979,11 @@ describe('ISet.entries', () => {
974
979
 
975
980
  expectType<
976
981
  typeof s0.entries,
977
- () => IterableIterator<readonly [1 | 2 | 3, 1 | 2 | 3]>
982
+ () => IterableIterator<FixedLengthTuple<2, 1 | 2 | 3>>
978
983
  >('<=');
979
984
 
980
- const result: DeepReadonly<[1 | 2 | 3, 1 | 2 | 3][]> = Array.from(
981
- s0.entries(),
982
- (x) => [x[0], x[1]],
983
- );
985
+ const result: DeepReadonly<MutableFixedLengthTuple<2, 1 | 2 | 3>[]> =
986
+ Array.from(s0.entries(), (x) => [x[0], x[1]]);
984
987
 
985
988
  assert.deepStrictEqual(
986
989
  result.toSorted((a, b) => a[0] - b[0]),
@@ -75,8 +75,10 @@ export type LevelOf<N> = [N] extends [SafeInt]
75
75
 
76
76
  // --- boolean fact algebra over a sign ----------------------------------------
77
77
 
78
- type And<A extends boolean, B extends boolean> = [A, B] extends [true, true]
79
- ? true
78
+ type And<A extends boolean, B extends boolean> = [A] extends [true]
79
+ ? [B] extends [true]
80
+ ? true
81
+ : false
80
82
  : false;
81
83
 
82
84
  type Or<A extends boolean, B extends boolean> = true extends A | B
@@ -170,8 +172,10 @@ type DivLevel<LA extends Level, LB extends Level> = 'number' extends LA | LB
170
172
  /** Floor division yields an integer; the safe range is preserved. */
171
173
  type DivIntLevel<LA extends Level, LB extends Level> = 'number' extends LA | LB
172
174
  ? 'number'
173
- : [LA, LB] extends ['safeint', 'safeint']
174
- ? 'safeint'
175
+ : [LA] extends ['safeint']
176
+ ? [LB] extends ['safeint']
177
+ ? 'safeint'
178
+ : 'int'
175
179
  : 'int';
176
180
 
177
181
  // --- reconstruction (sign, level) -> canonical brand -------------------------
@@ -1,11 +1,11 @@
1
- import { type NonZeroNumber } from 'ts-type-forge';
1
+ import { type FixedLengthTuple, type NonZeroNumber } from 'ts-type-forge';
2
2
  import { expectType } from '../expect-type.mjs';
3
3
  import { pipe, Result } from '../functional/index.mjs';
4
4
  import { asNonZeroFiniteNumber } from './branded-types/index.mjs';
5
5
  import { Num } from './num.mjs';
6
6
 
7
7
  const testClamp = (
8
- [a, b]: readonly [number, number],
8
+ [a, b]: FixedLengthTuple<2, number>,
9
9
  target: number,
10
10
  expected: number,
11
11
  ): void => {