ts-data-forge 14.0.0 → 14.1.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 +8 -7
- 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 +12 -4
- package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
- package/dist/array/impl/array-utils-transformation.mjs +4 -5
- package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
- package/dist/array/impl/array-utils-validation.d.mts +8 -3
- package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
- package/dist/array/impl/array-utils-validation.mjs.map +1 -1
- package/dist/collections/imap-mapped.mjs +1 -1
- package/dist/collections/imap-mapped.mjs.map +1 -1
- package/dist/collections/imap.d.mts +1 -1
- package/dist/collections/imap.mjs +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/functional/match.d.mts.map +1 -1
- package/dist/functional/match.mjs.map +1 -1
- package/dist/number/branded-types/int.d.mts +1 -1
- package/dist/number/branded-types/int16.d.mts +1 -1
- package/dist/number/branded-types/int32.d.mts +1 -1
- package/dist/number/branded-types/non-negative-int16.d.mts +1 -1
- package/dist/number/branded-types/non-negative-int32.d.mts +1 -1
- package/dist/number/branded-types/non-positive-finite-number.mjs +1 -1
- package/dist/number/branded-types/non-positive-finite-number.mjs.map +1 -1
- package/dist/number/branded-types/positive-int.d.mts +1 -1
- package/dist/number/branded-types/positive-int16.d.mts +1 -1
- package/dist/number/branded-types/positive-int32.d.mts +1 -1
- package/dist/number/branded-types/positive-safe-int.d.mts +1 -1
- package/dist/number/branded-types/positive-uint16.d.mts +1 -1
- package/dist/number/branded-types/positive-uint32.d.mts +1 -1
- package/dist/number/branded-types/safe-int.d.mts +1 -1
- package/dist/number/branded-types/safe-uint.d.mts +1 -1
- package/dist/number/branded-types/uint.d.mts +1 -1
- package/dist/number/branded-types/uint16.d.mts +1 -1
- package/dist/number/branded-types/uint32.d.mts +1 -1
- package/dist/number/enum/int8.d.mts +2 -2
- package/dist/number/enum/int8.d.mts.map +1 -1
- package/dist/number/enum/int8.mjs.map +1 -1
- package/dist/number/enum/uint8.d.mts +2 -2
- package/dist/number/enum/uint8.d.mts.map +1 -1
- package/dist/number/enum/uint8.mjs.map +1 -1
- package/dist/number/num-arithmetic-types.d.mts +9 -10
- package/dist/number/num-arithmetic-types.d.mts.map +1 -1
- package/dist/number/refined-number-utils.d.mts +2 -2
- package/dist/number/refined-number-utils.d.mts.map +1 -1
- package/dist/number/refined-number-utils.mjs.map +1 -1
- package/dist/object/object.d.mts +12 -12
- package/dist/object/object.d.mts.map +1 -1
- package/dist/object/object.mjs +5 -1
- package/dist/object/object.mjs.map +1 -1
- package/dist/others/fast-deep-equal.d.mts.map +1 -1
- package/dist/others/fast-deep-equal.mjs +2 -2
- package/dist/others/fast-deep-equal.mjs.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 +77 -4
- package/src/array/impl/array-utils-shape-invariants.test.mts +526 -0
- package/src/array/impl/array-utils-slice-clamped.mts +2 -1
- package/src/array/impl/array-utils-slicing.test.mts +11 -3
- package/src/array/impl/array-utils-transformation.mts +70 -20
- package/src/array/impl/array-utils-transformation.test.mts +44 -10
- package/src/array/impl/array-utils-validation.mts +54 -10
- package/src/array/impl/array-utils-validation.test.mts +153 -3
- package/src/collections/imap-mapped.mts +1 -1
- package/src/collections/imap-mapped.test.mts +2 -2
- package/src/collections/imap.mts +2 -2
- package/src/collections/iset-mapped.mts +7 -3
- package/src/collections/iset-mapped.test.mts +1 -1
- package/src/collections/iset.mts +7 -3
- package/src/collections/iset.test.mts +9 -6
- package/src/functional/match.mts +5 -4
- package/src/functional/result.test.mts +5 -2
- package/src/guard/is-primitive.test.mts +1 -1
- package/src/guard/is-record.test.mts +4 -4
- package/src/guard/is-type.test.mts +2 -2
- package/src/number/branded-types/finite-number.test.mts +2 -2
- package/src/number/branded-types/non-negative-finite-number.test.mts +2 -2
- package/src/number/branded-types/non-positive-finite-number.mts +1 -1
- package/src/number/branded-types/non-positive-finite-number.test.mts +5 -9
- package/src/number/branded-types/non-zero-finite-number.test.mts +2 -2
- package/src/number/branded-types/positive-finite-number.test.mts +2 -2
- package/src/number/enum/int8.mts +2 -1
- package/src/number/enum/uint8.mts +6 -2
- package/src/number/num-arithmetic-types.mts +129 -24
- package/src/number/num.test.mts +11 -11
- package/src/number/refined-number-utils.mts +2 -1
- package/src/object/object.mts +29 -17
- package/src/object/object.test.mts +2 -1
- package/src/others/fast-deep-equal.mts +6 -4
|
@@ -0,0 +1,526 @@
|
|
|
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 {
|
|
18
|
+
toFilled,
|
|
19
|
+
toInserted,
|
|
20
|
+
toPushed,
|
|
21
|
+
toRangeFilled,
|
|
22
|
+
toRemoved,
|
|
23
|
+
toUnshifted,
|
|
24
|
+
} from './array-utils-modification.mjs';
|
|
25
|
+
import { skip, skipLast, take, takeLast } from './array-utils-slicing.mjs';
|
|
26
|
+
import {
|
|
27
|
+
concat,
|
|
28
|
+
filter,
|
|
29
|
+
filterNot,
|
|
30
|
+
map,
|
|
31
|
+
toReversed,
|
|
32
|
+
toSorted,
|
|
33
|
+
toSortedBy,
|
|
34
|
+
uniq,
|
|
35
|
+
uniqBy,
|
|
36
|
+
} from './array-utils-transformation.mjs';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Invariant-based coverage for the array -> array transforms.
|
|
40
|
+
*
|
|
41
|
+
* Rather than pinning an expected type for every (brand x shape x context)
|
|
42
|
+
* combination — which is a large table whose expected side is easy to derive
|
|
43
|
+
* from the implementation and therefore prove nothing — this asserts the small
|
|
44
|
+
* set of properties every shape-preserving transform must satisfy, across a
|
|
45
|
+
* fixed list of sample inputs that covers the axes.
|
|
46
|
+
*
|
|
47
|
+
* The properties are:
|
|
48
|
+
*
|
|
49
|
+
* 1. **Length** — the result reports the same `length` as the input.
|
|
50
|
+
* 2. **Bounds** — a length brand on the input survives onto the result.
|
|
51
|
+
* 3. **Generic context** — from a caller that is itself generic over the
|
|
52
|
+
* array, the result is assignable to the caller's own annotation.
|
|
53
|
+
*
|
|
54
|
+
* Property 3 is the one that keeps regressing, and the one a per-combination
|
|
55
|
+
* table cannot express: a conditional a generic `Ar` cannot decide stays
|
|
56
|
+
* deferred, and a deferred conditional carrying a brand-intersection branch is
|
|
57
|
+
* not an array type at all — so the result has no `entries()`, is not
|
|
58
|
+
* assignable to the caller's annotation, and in the worst case the bound
|
|
59
|
+
* arithmetic behind the brand exceeds the instantiation limit.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
// The sample inputs. Between them they cover the axes: empty / singleton /
|
|
63
|
+
// fixed tuple / non-empty tuple / unbounded array on the shape axis, and
|
|
64
|
+
// unbranded / lower bound / upper bound / both / exact on the brand axis.
|
|
65
|
+
const emptyTuple: readonly [] = [] as const;
|
|
66
|
+
|
|
67
|
+
const singleton: readonly [1] = [1] as const;
|
|
68
|
+
|
|
69
|
+
const fixedTuple: readonly [1, 2, 3] = [1, 2, 3] as const;
|
|
70
|
+
|
|
71
|
+
const nonEmptyTuple: readonly [1, ...number[]] = [1, 2, 3] as const;
|
|
72
|
+
|
|
73
|
+
const plainArray: readonly number[] = [1, 2, 3] as const;
|
|
74
|
+
|
|
75
|
+
const lowerBounded: MinLengthArray<2, number> = asMinLengthArray(2, [1, 2, 3]);
|
|
76
|
+
|
|
77
|
+
const upperBounded: MaxLengthArray<5, number> = asMaxLengthArray(5, [1, 2, 3]);
|
|
78
|
+
|
|
79
|
+
const bounded: BoundedLengthArray<2, 5, number> = asBoundedLengthArray(
|
|
80
|
+
2,
|
|
81
|
+
5,
|
|
82
|
+
[1, 2, 3],
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const exactLength: FixedLengthArray<3, number> = asFixedLengthArray(
|
|
86
|
+
3,
|
|
87
|
+
[1, 2, 3],
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Property 3 — generic context.
|
|
92
|
+
*
|
|
93
|
+
* Each of these is an assertion by construction: the annotated return type is
|
|
94
|
+
* what the caller is entitled to expect, so the declaration only compiles while
|
|
95
|
+
* the transform reports something assignable to it. There is nothing to run.
|
|
96
|
+
*
|
|
97
|
+
* This list is the coverage record. Every array -> array transform that
|
|
98
|
+
* resolves under a generic array parameter appears here; the ones that do not
|
|
99
|
+
* are listed at the bottom of this file, with the reason.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
// Element-replacing transforms report the caller's own homomorphic mapping.
|
|
103
|
+
const mapInGenericContext = <const T extends readonly number[]>(
|
|
104
|
+
xs: T,
|
|
105
|
+
): Readonly<{ [K in keyof T]: string }> => map(xs, String);
|
|
106
|
+
|
|
107
|
+
const mapCurriedInGenericContext = <const T extends readonly number[]>(
|
|
108
|
+
xs: T,
|
|
109
|
+
): Readonly<{ [K in keyof T]: string }> => map(String)(xs);
|
|
110
|
+
|
|
111
|
+
const toFilledInGenericContext = <const T extends readonly number[]>(
|
|
112
|
+
xs: T,
|
|
113
|
+
): Readonly<{ [K in keyof T]: 0 }> => toFilled(xs, 0);
|
|
114
|
+
|
|
115
|
+
const toFilledCurriedInGenericContext = <const T extends readonly number[]>(
|
|
116
|
+
xs: T,
|
|
117
|
+
): Readonly<{ [K in keyof T]: 0 }> => toFilled(0)(xs);
|
|
118
|
+
|
|
119
|
+
const toRangeFilledInGenericContext = <const T extends readonly number[]>(
|
|
120
|
+
xs: T,
|
|
121
|
+
): Readonly<{ [K in keyof T]: number }> => toRangeFilled(xs, 0, [0, 1]);
|
|
122
|
+
|
|
123
|
+
// Reordering transforms keep the elements, so the caller is entitled to an
|
|
124
|
+
// array of them.
|
|
125
|
+
const toReversedInGenericContext = <const T extends readonly number[]>(
|
|
126
|
+
xs: T,
|
|
127
|
+
): readonly T[number][] => toReversed(xs);
|
|
128
|
+
|
|
129
|
+
const toSortedInGenericContext = <const T extends readonly number[]>(
|
|
130
|
+
xs: T,
|
|
131
|
+
): readonly T[number][] => toSorted(xs, (a, b) => a - b);
|
|
132
|
+
|
|
133
|
+
// Numbers sort without a comparator. This is the overload that makes it
|
|
134
|
+
// optional, which a generic caller could not reach at all while the parameter
|
|
135
|
+
// list was itself a conditional type.
|
|
136
|
+
const toSortedDefaultInGenericContext = <const T extends readonly number[]>(
|
|
137
|
+
xs: T,
|
|
138
|
+
): readonly T[number][] => toSorted(xs);
|
|
139
|
+
|
|
140
|
+
const toSortedNonNumberInGenericContext = <const T extends readonly string[]>(
|
|
141
|
+
xs: T,
|
|
142
|
+
): readonly T[number][] => toSorted(xs, (a, b) => a.localeCompare(b));
|
|
143
|
+
|
|
144
|
+
const toSortedByInGenericContext = <const T extends readonly number[]>(
|
|
145
|
+
xs: T,
|
|
146
|
+
): readonly T[number][] => toSortedBy(xs, (n) => n);
|
|
147
|
+
|
|
148
|
+
// Shortening transforms lose the length but keep the element type.
|
|
149
|
+
const takeInGenericContext = <const T extends readonly number[]>(
|
|
150
|
+
xs: T,
|
|
151
|
+
): readonly T[number][] => take(xs, 2);
|
|
152
|
+
|
|
153
|
+
const takeLastInGenericContext = <const T extends readonly number[]>(
|
|
154
|
+
xs: T,
|
|
155
|
+
): readonly T[number][] => takeLast(xs, 2);
|
|
156
|
+
|
|
157
|
+
const skipInGenericContext = <const T extends readonly number[]>(
|
|
158
|
+
xs: T,
|
|
159
|
+
): readonly T[number][] => skip(xs, 2);
|
|
160
|
+
|
|
161
|
+
const skipLastInGenericContext = <const T extends readonly number[]>(
|
|
162
|
+
xs: T,
|
|
163
|
+
): readonly T[number][] => skipLast(xs, 2);
|
|
164
|
+
|
|
165
|
+
const toRemovedInGenericContext = <const T extends readonly number[]>(
|
|
166
|
+
xs: T,
|
|
167
|
+
): readonly T[number][] => toRemoved(xs, 0);
|
|
168
|
+
|
|
169
|
+
const filterInGenericContext = <const T extends readonly number[]>(
|
|
170
|
+
xs: T,
|
|
171
|
+
): readonly T[number][] => filter(xs, (n) => n > 0);
|
|
172
|
+
|
|
173
|
+
const filterNotInGenericContext = <const T extends readonly number[]>(
|
|
174
|
+
xs: T,
|
|
175
|
+
): readonly T[number][] => filterNot(xs, (n) => n > 0);
|
|
176
|
+
|
|
177
|
+
const uniqInGenericContext = <const T extends readonly number[]>(
|
|
178
|
+
xs: T,
|
|
179
|
+
): readonly T[number][] => uniq(xs);
|
|
180
|
+
|
|
181
|
+
const uniqByInGenericContext = <const T extends readonly number[]>(
|
|
182
|
+
xs: T,
|
|
183
|
+
): readonly T[number][] => uniqBy(xs, (n) => n);
|
|
184
|
+
|
|
185
|
+
// Lengthening transforms widen the element type by whatever was added.
|
|
186
|
+
const toPushedInGenericContext = <const T extends readonly number[]>(
|
|
187
|
+
xs: T,
|
|
188
|
+
): readonly (T[number] | 'x')[] => toPushed(xs, 'x');
|
|
189
|
+
|
|
190
|
+
const toUnshiftedInGenericContext = <const T extends readonly number[]>(
|
|
191
|
+
xs: T,
|
|
192
|
+
): readonly (T[number] | 'x')[] => toUnshifted(xs, 'x');
|
|
193
|
+
|
|
194
|
+
const toInsertedInGenericContext = <const T extends readonly number[]>(
|
|
195
|
+
xs: T,
|
|
196
|
+
): readonly (T[number] | 'x')[] => toInserted(xs, 0, 'x');
|
|
197
|
+
|
|
198
|
+
const concatInGenericContext = <const T extends readonly number[]>(
|
|
199
|
+
xs: T,
|
|
200
|
+
): readonly T[number][] => concat(xs, xs);
|
|
201
|
+
|
|
202
|
+
describe('Arr shape invariants', () => {
|
|
203
|
+
// The type-level assertions below are checked by `tsc`; this pins the same
|
|
204
|
+
// property at runtime, so a transform that silently drops or adds an element
|
|
205
|
+
// fails here even where the type says otherwise.
|
|
206
|
+
test('every shape-preserving transform keeps the length at runtime', () => {
|
|
207
|
+
const input = [3, 1, 2] as const;
|
|
208
|
+
|
|
209
|
+
assert.deepStrictEqual<FixedLengthTuple<6, 3>>(
|
|
210
|
+
[
|
|
211
|
+
map(input, String).length,
|
|
212
|
+
toFilled(input, 0).length,
|
|
213
|
+
toRangeFilled(input, 0, [1, 2]).length,
|
|
214
|
+
toSorted(input).length,
|
|
215
|
+
toSortedBy(input, (n) => n).length,
|
|
216
|
+
toReversed(input).length,
|
|
217
|
+
] as const,
|
|
218
|
+
[3, 3, 3, 3, 3, 3] as const,
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('the element-replacing callers agree with their annotations', () => {
|
|
223
|
+
const input = [3, 1, 2] as const;
|
|
224
|
+
|
|
225
|
+
assert.deepStrictEqual<FixedLengthTuple<3, string>>(
|
|
226
|
+
mapInGenericContext(input),
|
|
227
|
+
['3', '1', '2'],
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
assert.deepStrictEqual<FixedLengthTuple<3, string>>(
|
|
231
|
+
mapCurriedInGenericContext(input),
|
|
232
|
+
['3', '1', '2'],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
assert.deepStrictEqual<FixedLengthTuple<3, 0>>(
|
|
236
|
+
toFilledInGenericContext(input),
|
|
237
|
+
[0, 0, 0],
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
assert.deepStrictEqual<FixedLengthTuple<3, 0>>(
|
|
241
|
+
toFilledCurriedInGenericContext(input),
|
|
242
|
+
[0, 0, 0],
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
assert.deepStrictEqual<FixedLengthTuple<3, number>>(
|
|
246
|
+
toRangeFilledInGenericContext(input),
|
|
247
|
+
[0, 1, 2],
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('the reordering callers agree with their annotations', () => {
|
|
252
|
+
const input = [3, 1, 2] as const;
|
|
253
|
+
|
|
254
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
255
|
+
toReversedInGenericContext(input),
|
|
256
|
+
[2, 1, 3],
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
260
|
+
toSortedInGenericContext(input),
|
|
261
|
+
[1, 2, 3],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
265
|
+
toSortedDefaultInGenericContext(input),
|
|
266
|
+
[1, 2, 3],
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
assert.deepStrictEqual<readonly string[]>(
|
|
270
|
+
toSortedNonNumberInGenericContext(['c', 'a', 'b'] as const),
|
|
271
|
+
['a', 'b', 'c'],
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
275
|
+
toSortedByInGenericContext(input),
|
|
276
|
+
[1, 2, 3],
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('the shortening callers agree with their annotations', () => {
|
|
281
|
+
const input = [3, 1, 2] as const;
|
|
282
|
+
|
|
283
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
284
|
+
takeInGenericContext(input),
|
|
285
|
+
[3, 1],
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
289
|
+
takeLastInGenericContext(input),
|
|
290
|
+
[1, 2],
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
assert.deepStrictEqual<readonly number[]>(skipInGenericContext(input), [2]);
|
|
294
|
+
|
|
295
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
296
|
+
skipLastInGenericContext(input),
|
|
297
|
+
[3],
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
301
|
+
toRemovedInGenericContext(input),
|
|
302
|
+
[1, 2],
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
306
|
+
filterInGenericContext(input),
|
|
307
|
+
[3, 1, 2],
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
311
|
+
filterNotInGenericContext(input),
|
|
312
|
+
[],
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
316
|
+
uniqInGenericContext([1, 1, 2] as const),
|
|
317
|
+
[1, 2],
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
321
|
+
uniqByInGenericContext([1, 1, 2] as const),
|
|
322
|
+
[1, 2],
|
|
323
|
+
);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('the lengthening callers agree with their annotations', () => {
|
|
327
|
+
const input = [3, 1, 2] as const;
|
|
328
|
+
|
|
329
|
+
assert.deepStrictEqual<readonly (number | string)[]>(
|
|
330
|
+
toPushedInGenericContext(input),
|
|
331
|
+
[3, 1, 2, 'x'],
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
assert.deepStrictEqual<readonly (number | string)[]>(
|
|
335
|
+
toUnshiftedInGenericContext(input),
|
|
336
|
+
['x', 3, 1, 2],
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
assert.deepStrictEqual<readonly (number | string)[]>(
|
|
340
|
+
toInsertedInGenericContext(input),
|
|
341
|
+
['x', 3, 1, 2],
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
assert.deepStrictEqual<readonly number[]>(
|
|
345
|
+
concatInGenericContext(input),
|
|
346
|
+
[3, 1, 2, 3, 1, 2],
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
// `map` — property 1 (length) across every unbranded sample.
|
|
352
|
+
{
|
|
353
|
+
const _a = map(emptyTuple, String);
|
|
354
|
+
|
|
355
|
+
const _b = map(singleton, String);
|
|
356
|
+
|
|
357
|
+
const _c = map(fixedTuple, String);
|
|
358
|
+
|
|
359
|
+
const _d = map(nonEmptyTuple, String);
|
|
360
|
+
|
|
361
|
+
const _e = map(plainArray, String);
|
|
362
|
+
|
|
363
|
+
expectType<(typeof _a)['length'], (typeof emptyTuple)['length']>('=');
|
|
364
|
+
|
|
365
|
+
expectType<(typeof _b)['length'], (typeof singleton)['length']>('=');
|
|
366
|
+
|
|
367
|
+
expectType<(typeof _c)['length'], (typeof fixedTuple)['length']>('=');
|
|
368
|
+
|
|
369
|
+
expectType<(typeof _d)['length'], (typeof nonEmptyTuple)['length']>('=');
|
|
370
|
+
|
|
371
|
+
expectType<(typeof _e)['length'], (typeof plainArray)['length']>('=');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// `map` — property 2 (bounds) across every branded sample.
|
|
375
|
+
{
|
|
376
|
+
const _a = map(lowerBounded, String);
|
|
377
|
+
|
|
378
|
+
const _b = map(upperBounded, String);
|
|
379
|
+
|
|
380
|
+
const _c = map(bounded, String);
|
|
381
|
+
|
|
382
|
+
const _d = map(exactLength, String);
|
|
383
|
+
|
|
384
|
+
expectType<MinLengthOf<typeof _a>, MinLengthOf<typeof lowerBounded>>('=');
|
|
385
|
+
|
|
386
|
+
expectType<MaxLengthOf<typeof _b>, MaxLengthOf<typeof upperBounded>>('=');
|
|
387
|
+
|
|
388
|
+
expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof bounded>>('=');
|
|
389
|
+
|
|
390
|
+
expectType<MaxLengthOf<typeof _c>, MaxLengthOf<typeof bounded>>('=');
|
|
391
|
+
|
|
392
|
+
expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof exactLength>>('=');
|
|
393
|
+
|
|
394
|
+
expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof exactLength>>('=');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// `toFilled` — properties 1 and 2.
|
|
398
|
+
{
|
|
399
|
+
const _a = toFilled(fixedTuple, 0);
|
|
400
|
+
|
|
401
|
+
const _b = toFilled(nonEmptyTuple, 0);
|
|
402
|
+
|
|
403
|
+
const _c = toFilled(plainArray, 0);
|
|
404
|
+
|
|
405
|
+
const _d = toFilled(lowerBounded, 0);
|
|
406
|
+
|
|
407
|
+
const _e = toFilled(bounded, 0);
|
|
408
|
+
|
|
409
|
+
expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
|
|
410
|
+
|
|
411
|
+
expectType<(typeof _b)['length'], (typeof nonEmptyTuple)['length']>('=');
|
|
412
|
+
|
|
413
|
+
expectType<(typeof _c)['length'], (typeof plainArray)['length']>('=');
|
|
414
|
+
|
|
415
|
+
expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof lowerBounded>>('=');
|
|
416
|
+
|
|
417
|
+
expectType<MinLengthOf<typeof _e>, MinLengthOf<typeof bounded>>('=');
|
|
418
|
+
|
|
419
|
+
expectType<MaxLengthOf<typeof _e>, MaxLengthOf<typeof bounded>>('=');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// `toRangeFilled` — properties 1 and 2.
|
|
423
|
+
{
|
|
424
|
+
const _a = toRangeFilled(fixedTuple, 0, [1, 2]);
|
|
425
|
+
|
|
426
|
+
const _b = toRangeFilled(plainArray, 0, [0, 1]);
|
|
427
|
+
|
|
428
|
+
const _c = toRangeFilled(lowerBounded, 0, [0, 1]);
|
|
429
|
+
|
|
430
|
+
expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
|
|
431
|
+
|
|
432
|
+
expectType<(typeof _b)['length'], (typeof plainArray)['length']>('=');
|
|
433
|
+
|
|
434
|
+
expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// `toSorted` — properties 1 and 2. Sorting neither adds nor drops elements.
|
|
438
|
+
{
|
|
439
|
+
const _a = toSorted(fixedTuple);
|
|
440
|
+
|
|
441
|
+
const _b = toSorted(nonEmptyTuple);
|
|
442
|
+
|
|
443
|
+
const _c = toSorted(plainArray);
|
|
444
|
+
|
|
445
|
+
const _d = toSorted(lowerBounded);
|
|
446
|
+
|
|
447
|
+
const _e = toSorted(bounded);
|
|
448
|
+
|
|
449
|
+
expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
|
|
450
|
+
|
|
451
|
+
expectType<(typeof _b)['length'], (typeof nonEmptyTuple)['length']>('=');
|
|
452
|
+
|
|
453
|
+
expectType<(typeof _c)['length'], (typeof plainArray)['length']>('=');
|
|
454
|
+
|
|
455
|
+
expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof lowerBounded>>('=');
|
|
456
|
+
|
|
457
|
+
expectType<MinLengthOf<typeof _e>, MinLengthOf<typeof bounded>>('=');
|
|
458
|
+
|
|
459
|
+
expectType<MaxLengthOf<typeof _e>, MaxLengthOf<typeof bounded>>('=');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// `toSortedBy` — properties 1 and 2.
|
|
463
|
+
{
|
|
464
|
+
const _a = toSortedBy(fixedTuple, (n) => n);
|
|
465
|
+
|
|
466
|
+
const _b = toSortedBy(plainArray, (n) => n);
|
|
467
|
+
|
|
468
|
+
const _c = toSortedBy(lowerBounded, (n) => n);
|
|
469
|
+
|
|
470
|
+
const _d = toSortedBy(bounded, (n) => n);
|
|
471
|
+
|
|
472
|
+
expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
|
|
473
|
+
|
|
474
|
+
expectType<(typeof _b)['length'], (typeof plainArray)['length']>('=');
|
|
475
|
+
|
|
476
|
+
expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
|
|
477
|
+
|
|
478
|
+
expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof bounded>>('=');
|
|
479
|
+
|
|
480
|
+
expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof bounded>>('=');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// `toReversed` — properties 1 and 2. Reversal permutes, so the length and the
|
|
484
|
+
// bounds are exactly what must not move.
|
|
485
|
+
{
|
|
486
|
+
const _a = toReversed(fixedTuple);
|
|
487
|
+
|
|
488
|
+
const _b = toReversed(plainArray);
|
|
489
|
+
|
|
490
|
+
const _c = toReversed(lowerBounded);
|
|
491
|
+
|
|
492
|
+
const _d = toReversed(bounded);
|
|
493
|
+
|
|
494
|
+
expectType<(typeof _a)['length'], (typeof fixedTuple)['length']>('=');
|
|
495
|
+
|
|
496
|
+
expectType<(typeof _b)['length'], (typeof plainArray)['length']>('=');
|
|
497
|
+
|
|
498
|
+
expectType<MinLengthOf<typeof _c>, MinLengthOf<typeof lowerBounded>>('=');
|
|
499
|
+
|
|
500
|
+
expectType<MinLengthOf<typeof _d>, MinLengthOf<typeof bounded>>('=');
|
|
501
|
+
|
|
502
|
+
expectType<MaxLengthOf<typeof _d>, MaxLengthOf<typeof bounded>>('=');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The transforms that do **not** resolve under a generic array parameter, and
|
|
507
|
+
* therefore have no property-3 entry above:
|
|
508
|
+
*
|
|
509
|
+
* | transform | reports | symptom under a generic `Ar` |
|
|
510
|
+
* | :------------------ | :---------------------- | :--------------------------- |
|
|
511
|
+
* | `tail` / `butLast` | `ConstrainedList.Tail` | TS2590 |
|
|
512
|
+
* | `zip` | `ConstrainedList.Zip` | TS2589, and no `entries()` |
|
|
513
|
+
* | `set` / `toUpdated` | `ConstrainedList.SetAt` | TS2589 |
|
|
514
|
+
*
|
|
515
|
+
* The cause is the same in all three, and it is upstream. Their brand branch
|
|
516
|
+
* does arithmetic on the input's own bounds (`SubLength`, `SmallerLength`), and
|
|
517
|
+
* that arithmetic goes through `MakeTuple`. For a concrete branded array the
|
|
518
|
+
* bound is a single number and the expansion is cheap; for a type parameter it
|
|
519
|
+
* is the whole `SupportedLength` union, which `MakeTuple` distributes over.
|
|
520
|
+
*
|
|
521
|
+
* `map`, `toFilled` and `toRangeFilled` were fixable in 14.0.1 precisely
|
|
522
|
+
* because `ChangeArrayElement`'s brand branch does no such arithmetic. The
|
|
523
|
+
* `UnknownBrand`-keyed overload split that fixed them does not transfer: the
|
|
524
|
+
* constrained overload makes the checker attempt that arithmetic at every call
|
|
525
|
+
* site, generic or not.
|
|
526
|
+
*/
|
|
@@ -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:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type MinLengthArray } from 'ts-type-forge';
|
|
2
2
|
import { expectType } from '../../expect-type.mjs';
|
|
3
|
+
import { asMinLengthArray } from './array-utils-length-bounded-array-cast.mjs';
|
|
3
4
|
import {
|
|
4
5
|
butLast,
|
|
5
6
|
skip,
|
|
@@ -224,9 +225,9 @@ describe('Arr slicing', () => {
|
|
|
224
225
|
/* Length-constrained inputs. `List.*` reports what it can see from a
|
|
225
226
|
fixed-length tuple, and a branded array's `length` is `number`, so it handed
|
|
226
227
|
the input type straight back — `tail` of an "at least 5" array claimed the
|
|
227
|
-
four-element result was still at least five.
|
|
228
|
-
`array-utils-modification.test.mts`. */
|
|
229
|
-
|
|
228
|
+
four-element result was still at least five. Shape-per-parameter; see the
|
|
229
|
+
note in `array-utils-modification.test.mts`. */
|
|
230
|
+
const slicingTypeChecks = (
|
|
230
231
|
brandedFive: MinLengthArray<5, string>,
|
|
231
232
|
brandedAndTuple: MinLengthArray<3, number> & readonly [1, 2, 3, 4, 5],
|
|
232
233
|
): void => {
|
|
@@ -274,3 +275,10 @@ export const slicingTypeChecks = (
|
|
|
274
275
|
|
|
275
276
|
expectType<typeof _takenTuple, readonly [1, 2]>('=');
|
|
276
277
|
};
|
|
278
|
+
|
|
279
|
+
test('slicing type-level checks', () => {
|
|
280
|
+
slicingTypeChecks(
|
|
281
|
+
asMinLengthArray(5, ['a', 'b', 'c', 'd', 'e'] as const),
|
|
282
|
+
asMinLengthArray(3, [1, 2, 3, 4, 5] as const),
|
|
283
|
+
);
|
|
284
|
+
});
|
|
@@ -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:
|
|
@@ -218,28 +245,51 @@ export const toReversed = <const Ar extends readonly unknown[]>(
|
|
|
218
245
|
* assert.deepStrictEqual(alphabetical, expectedWords);
|
|
219
246
|
* ```
|
|
220
247
|
*/
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
// Numbers sort without a comparator, so it is optional for them and required
|
|
249
|
+
// for everything else.
|
|
250
|
+
//
|
|
251
|
+
// Stated as two overloads rather than one conditional rest-parameter tuple.
|
|
252
|
+
// The conditional form reads well, but a *generic* `Ar` cannot decide it, so
|
|
253
|
+
// the parameter list stayed unresolved and `Arr.toSorted(xs, cmp)` inside a
|
|
254
|
+
// function that is itself generic over the array was rejected outright:
|
|
255
|
+
//
|
|
256
|
+
// Argument of type '[T, (a: number, b: number) => number]' is not
|
|
257
|
+
// assignable to parameter of type 'T extends readonly number[] ? ... : ...'
|
|
258
|
+
//
|
|
259
|
+
// Overload resolution picks a branch per call instead, so both concrete and
|
|
260
|
+
// generic callers work. The *return* type stays conditional — that one a
|
|
261
|
+
// generic caller handles, as `toSortedBy` (same shape) already showed.
|
|
262
|
+
export function toSorted<const Ar extends readonly number[]>(
|
|
263
|
+
array: Ar,
|
|
264
|
+
comparator?: (x: Ar[number], y: Ar[number]) => number,
|
|
265
|
+
): SortedResult<Ar>;
|
|
266
|
+
|
|
267
|
+
export function toSorted<const Ar extends readonly unknown[]>(
|
|
268
|
+
array: Ar,
|
|
269
|
+
comparator: (x: Ar[number], y: Ar[number]) => number,
|
|
270
|
+
): SortedResult<Ar>;
|
|
271
|
+
|
|
272
|
+
export function toSorted(
|
|
273
|
+
array: readonly unknown[],
|
|
274
|
+
comparator?: (x: never, y: never) => number,
|
|
275
|
+
): readonly unknown[] {
|
|
276
|
+
return array.toSorted(
|
|
238
277
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
239
278
|
(comparator as ((x: unknown, y: unknown) => number) | undefined) ??
|
|
240
279
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
241
280
|
((x, y) => (x as number) - (y as number)),
|
|
242
|
-
)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The result of sorting `Ar`: same elements, same length, same bounds. */
|
|
285
|
+
type SortedResult<Ar extends readonly unknown[]> =
|
|
286
|
+
HasLengthConstraint<Ar> extends true
|
|
287
|
+
? ChangeArrayElement<Ar, Ar[number]>
|
|
288
|
+
: IsFixedLengthList<Ar> extends true
|
|
289
|
+
? FixedLengthTuple<Ar['length'], Ar[number]>
|
|
290
|
+
: Ar extends NonEmptyTuple<unknown>
|
|
291
|
+
? NonEmptyArray<Ar[number]>
|
|
292
|
+
: readonly Ar[number][];
|
|
243
293
|
|
|
244
294
|
/**
|
|
245
295
|
* Sorts an array by a mapped value.
|