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.
- package/README.md +3 -2
- package/dist/array/impl/array-utils-modification.d.mts +18 -5
- package/dist/array/impl/array-utils-modification.d.mts.map +1 -1
- package/dist/array/impl/array-utils-modification.mjs.map +1 -1
- package/dist/array/impl/array-utils-slice-clamped.d.mts.map +1 -1
- package/dist/array/impl/array-utils-slice-clamped.mjs.map +1 -1
- package/dist/array/impl/array-utils-transformation.d.mts +8 -3
- package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
- package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
- package/dist/collections/imap.d.mts +1 -1
- package/dist/collections/imap.mjs.map +1 -1
- package/dist/collections/iset-mapped.d.mts +2 -2
- package/dist/collections/iset-mapped.d.mts.map +1 -1
- package/dist/collections/iset-mapped.mjs.map +1 -1
- package/dist/collections/iset.d.mts +2 -2
- package/dist/collections/iset.d.mts.map +1 -1
- package/dist/collections/iset.mjs.map +1 -1
- package/dist/number/num-arithmetic-types.d.mts +2 -2
- package/dist/number/num-arithmetic-types.d.mts.map +1 -1
- package/package.json +4 -5
- package/src/array/impl/array-utils-creation.test.mts +10 -8
- package/src/array/impl/array-utils-length-constraint-preservation.test.mts +2 -2
- package/src/array/impl/array-utils-modification.mts +41 -33
- package/src/array/impl/array-utils-modification.test.mts +52 -0
- package/src/array/impl/array-utils-shape-invariants.test.mts +284 -0
- package/src/array/impl/array-utils-slice-clamped.mts +2 -1
- package/src/array/impl/array-utils-transformation.mts +29 -2
- package/src/array/impl/array-utils-transformation.test.mts +34 -7
- package/src/collections/imap.mts +1 -1
- package/src/collections/iset-mapped.mts +7 -3
- package/src/collections/iset.mts +7 -3
- package/src/collections/iset.test.mts +9 -6
- package/src/number/num-arithmetic-types.mts +8 -4
- 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
|
-
|
|
|
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[]>(
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
1045
|
+
expectType<typeof zipped, readonly FixedLengthTuple<2, number>[]>('=');
|
|
1019
1046
|
|
|
1020
1047
|
test('case 2', () => {
|
|
1021
1048
|
assert.deepStrictEqual(zipped, [[1, 4]]);
|
package/src/collections/imap.mts
CHANGED
|
@@ -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
|
|
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 {
|
|
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<
|
|
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<
|
|
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);
|
package/src/collections/iset.mts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/* eslint-disable unicorn/prefer-iterator-to-array-at-end */
|
|
2
|
-
import {
|
|
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<
|
|
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<
|
|
920
|
+
entries(): IterableIterator<FixedLengthTuple<2, K>> {
|
|
917
921
|
return this.#set.entries();
|
|
918
922
|
}
|
|
919
923
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
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<
|
|
982
|
+
() => IterableIterator<FixedLengthTuple<2, 1 | 2 | 3>>
|
|
978
983
|
>('<=');
|
|
979
984
|
|
|
980
|
-
const result: DeepReadonly<
|
|
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
|
|
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
|
|
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 -------------------------
|
package/src/number/num.test.mts
CHANGED
|
@@ -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]:
|
|
8
|
+
[a, b]: FixedLengthTuple<2, number>,
|
|
9
9
|
target: number,
|
|
10
10
|
expected: number,
|
|
11
11
|
): void => {
|