ts-data-forge 13.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 (93) hide show
  1. package/README.md +4 -3
  2. package/dist/array/impl/array-utils-creation.d.mts +2 -2
  3. package/dist/array/impl/array-utils-creation.d.mts.map +1 -1
  4. package/dist/array/impl/array-utils-creation.mjs.map +1 -1
  5. package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts +56 -29
  6. package/dist/array/impl/array-utils-length-bounded-array-cast.d.mts.map +1 -1
  7. package/dist/array/impl/array-utils-length-bounded-array-cast.mjs +23 -15
  8. package/dist/array/impl/array-utils-length-bounded-array-cast.mjs.map +1 -1
  9. package/dist/array/impl/array-utils-length-bounded-array-guard.d.mts +126 -28
  10. package/dist/array/impl/array-utils-length-bounded-array-guard.d.mts.map +1 -1
  11. package/dist/array/impl/array-utils-length-bounded-array-guard.mjs +39 -5
  12. package/dist/array/impl/array-utils-length-bounded-array-guard.mjs.map +1 -1
  13. package/dist/array/impl/array-utils-length-bounded-tuple-cast.d.mts +167 -0
  14. package/dist/array/impl/array-utils-length-bounded-tuple-cast.d.mts.map +1 -0
  15. package/dist/array/impl/array-utils-length-bounded-tuple-cast.mjs +87 -0
  16. package/dist/array/impl/array-utils-length-bounded-tuple-cast.mjs.map +1 -0
  17. package/dist/array/impl/array-utils-length-bounded-tuple-guard.d.mts +135 -0
  18. package/dist/array/impl/array-utils-length-bounded-tuple-guard.d.mts.map +1 -0
  19. package/dist/array/impl/array-utils-length-bounded-tuple-guard.mjs +41 -0
  20. package/dist/array/impl/array-utils-length-bounded-tuple-guard.mjs.map +1 -0
  21. package/dist/array/impl/array-utils-modification.d.mts +45 -18
  22. package/dist/array/impl/array-utils-modification.d.mts.map +1 -1
  23. package/dist/array/impl/array-utils-modification.mjs.map +1 -1
  24. package/dist/array/impl/array-utils-reducing-value.d.mts +3 -3
  25. package/dist/array/impl/array-utils-reducing-value.d.mts.map +1 -1
  26. package/dist/array/impl/array-utils-reducing-value.mjs +1 -1
  27. package/dist/array/impl/array-utils-reducing-value.mjs.map +1 -1
  28. package/dist/array/impl/array-utils-slice-clamped.d.mts.map +1 -1
  29. package/dist/array/impl/array-utils-slice-clamped.mjs.map +1 -1
  30. package/dist/array/impl/array-utils-slicing.d.mts +11 -11
  31. package/dist/array/impl/array-utils-slicing.d.mts.map +1 -1
  32. package/dist/array/impl/array-utils-slicing.mjs +1 -1
  33. package/dist/array/impl/array-utils-slicing.mjs.map +1 -1
  34. package/dist/array/impl/array-utils-transformation.d.mts +36 -19
  35. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  36. package/dist/array/impl/array-utils-transformation.mjs +2 -2
  37. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  38. package/dist/array/impl/array-utils-validation.d.mts +2 -124
  39. package/dist/array/impl/array-utils-validation.d.mts.map +1 -1
  40. package/dist/array/impl/array-utils-validation.mjs +1 -7
  41. package/dist/array/impl/array-utils-validation.mjs.map +1 -1
  42. package/dist/array/impl/index.d.mts +2 -0
  43. package/dist/array/impl/index.d.mts.map +1 -1
  44. package/dist/array/impl/index.mjs +5 -3
  45. package/dist/array/impl/index.mjs.map +1 -1
  46. package/dist/collections/imap.d.mts +1 -1
  47. package/dist/collections/imap.mjs.map +1 -1
  48. package/dist/collections/iset-mapped.d.mts +2 -2
  49. package/dist/collections/iset-mapped.d.mts.map +1 -1
  50. package/dist/collections/iset-mapped.mjs.map +1 -1
  51. package/dist/collections/iset.d.mts +2 -2
  52. package/dist/collections/iset.d.mts.map +1 -1
  53. package/dist/collections/iset.mjs.map +1 -1
  54. package/dist/number/num-arithmetic-types.d.mts +2 -2
  55. package/dist/number/num-arithmetic-types.d.mts.map +1 -1
  56. package/dist/object/object.mjs +1 -1
  57. package/dist/string/str.d.mts +44 -44
  58. package/dist/string/str.d.mts.map +1 -1
  59. package/dist/string/str.mjs +17 -17
  60. package/dist/string/str.mjs.map +1 -1
  61. package/package.json +4 -5
  62. package/src/array/impl/array-utils-creation.mts +7 -3
  63. package/src/array/impl/array-utils-creation.test.mts +10 -8
  64. package/src/array/impl/array-utils-length-bounded-array-cast.mts +88 -50
  65. package/src/array/impl/array-utils-length-bounded-array-cast.test.mts +40 -13
  66. package/src/array/impl/array-utils-length-bounded-array-guard.mts +248 -39
  67. package/src/array/impl/array-utils-length-bounded-array-guard.test.mts +135 -24
  68. package/src/array/impl/array-utils-length-bounded-tuple-cast.mts +364 -0
  69. package/src/array/impl/array-utils-length-bounded-tuple-cast.test.mts +207 -0
  70. package/src/array/impl/array-utils-length-bounded-tuple-guard.mts +266 -0
  71. package/src/array/impl/array-utils-length-bounded-tuple-guard.test.mts +441 -0
  72. package/src/array/impl/array-utils-length-constraint-preservation.test.mts +287 -0
  73. package/src/array/impl/array-utils-modification.mts +84 -61
  74. package/src/array/impl/array-utils-modification.test.mts +145 -6
  75. package/src/array/impl/array-utils-reducing-value.mts +5 -5
  76. package/src/array/impl/array-utils-shape-invariants.test.mts +284 -0
  77. package/src/array/impl/array-utils-slice-clamped.mts +2 -1
  78. package/src/array/impl/array-utils-slicing.mts +65 -43
  79. package/src/array/impl/array-utils-slicing.test.mts +55 -0
  80. package/src/array/impl/array-utils-transformation.mts +91 -37
  81. package/src/array/impl/array-utils-transformation.test.mts +96 -23
  82. package/src/array/impl/array-utils-validation.mts +2 -162
  83. package/src/array/impl/array-utils-validation.test.mts +0 -371
  84. package/src/array/impl/index.mts +2 -0
  85. package/src/collections/imap.mts +1 -1
  86. package/src/collections/iset-mapped.mts +7 -3
  87. package/src/collections/iset.mts +7 -3
  88. package/src/collections/iset.test.mts +9 -6
  89. package/src/collections/queue.test.mts +1 -1
  90. package/src/number/num-arithmetic-types.mts +8 -4
  91. package/src/number/num.test.mts +2 -2
  92. package/src/string/str.mts +69 -69
  93. package/src/string/str.test.mts +40 -40
@@ -0,0 +1,287 @@
1
+ import {
2
+ type BoundedLengthArray,
3
+ type FixedLengthArray,
4
+ type FixedLengthTuple,
5
+ type MaxLengthArray,
6
+ type MinLengthArray,
7
+ type Seq,
8
+ } from 'ts-type-forge';
9
+ import { expectType } from '../../expect-type.mjs';
10
+ import { copy, create, seq, zeros } from './array-utils-creation.mjs';
11
+ import {
12
+ asBoundedLengthArray,
13
+ asFixedLengthArray,
14
+ asMaxLengthArray,
15
+ asMinLengthArray,
16
+ } from './array-utils-length-bounded-array-cast.mjs';
17
+ import {
18
+ set,
19
+ toFilled,
20
+ toRangeFilled,
21
+ toUpdated,
22
+ } from './array-utils-modification.mjs';
23
+ import {
24
+ map,
25
+ partition,
26
+ toReversed,
27
+ toSorted,
28
+ toSortedBy,
29
+ } from './array-utils-transformation.mjs';
30
+
31
+ describe('map preserves length constraints', () => {
32
+ test('a branded input keeps its constraint', () => {
33
+ const input: MinLengthArray<3, number> = asMinLengthArray(3, [0, 1, 2, 3]);
34
+
35
+ const result = map(input, (value) => `#${String(value)}`);
36
+
37
+ expectType<typeof result, MinLengthArray<3, string>>('~=');
38
+
39
+ // The structural prefix survives, so in-range access is not `undefined`.
40
+ const first: string = result[0];
41
+
42
+ assert.deepStrictEqual(first, '#0');
43
+
44
+ assert.deepStrictEqual<readonly string[]>(result, ['#0', '#1', '#2', '#3']);
45
+ });
46
+
47
+ test('a fixed-length branded input keeps its exact length', () => {
48
+ const input: FixedLengthArray<3, number> = asFixedLengthArray(
49
+ 3,
50
+ [255, 128, 0],
51
+ );
52
+
53
+ const result = map(input, (value) => value / 255);
54
+
55
+ expectType<typeof result, FixedLengthArray<3, number>>('~=');
56
+
57
+ const length: 3 = result.length;
58
+
59
+ assert.deepStrictEqual(length, 3);
60
+ });
61
+
62
+ test('a minimum above the structural prefix cap survives, clamped like MinLengthArray itself', () => {
63
+ const input: MinLengthArray<12, number> = asMinLengthArray(
64
+ 12,
65
+ [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
66
+ );
67
+
68
+ const result = map(input, String);
69
+
70
+ expectType<typeof result, MinLengthArray<12, string>>('~=');
71
+
72
+ assert.deepStrictEqual<readonly string[]>(result, [
73
+ '0',
74
+ '1',
75
+ '2',
76
+ '3',
77
+ '4',
78
+ '5',
79
+ '6',
80
+ '7',
81
+ '8',
82
+ '9',
83
+ '10',
84
+ '11',
85
+ ]);
86
+ });
87
+
88
+ test('a plain tuple still maps element-wise', () => {
89
+ const result = map([1, 2, 3] as const, String);
90
+
91
+ expectType<typeof result, FixedLengthTuple<3, string>>('=');
92
+
93
+ assert.deepStrictEqual(result, ['1', '2', '3']);
94
+ });
95
+
96
+ test('a plain array still maps to a plain array', () => {
97
+ const input: readonly number[] = [1, 2, 3] as const;
98
+
99
+ const result = map(input, String);
100
+
101
+ expectType<typeof result, readonly string[]>('=');
102
+
103
+ assert.deepStrictEqual(result, ['1', '2', '3']);
104
+ });
105
+
106
+ test('an upper bound survives too', () => {
107
+ const input: MaxLengthArray<5, number> = asMaxLengthArray(5, [1, 2]);
108
+
109
+ const result = map(input, String);
110
+
111
+ expectType<typeof result, MaxLengthArray<5, string>>('~=');
112
+
113
+ assert.deepStrictEqual<readonly string[]>(result, ['1', '2']);
114
+ });
115
+
116
+ test('both bounds survive together', () => {
117
+ const input: BoundedLengthArray<2, 5, number> = asBoundedLengthArray(
118
+ 2,
119
+ 5,
120
+ [1, 2],
121
+ );
122
+
123
+ const result = map(input, String);
124
+
125
+ expectType<typeof result, BoundedLengthArray<2, 5, string>>('~=');
126
+
127
+ assert.deepStrictEqual<readonly string[]>(result, ['1', '2']);
128
+ });
129
+ });
130
+
131
+ describe('other length-preserving utilities preserve length constraints', () => {
132
+ test('toSorted keeps the brand', () => {
133
+ const input: FixedLengthArray<3, number> = asFixedLengthArray(3, [3, 1, 2]);
134
+
135
+ const result = toSorted(input, (x, y) => x - y);
136
+
137
+ expectType<typeof result, FixedLengthArray<3, number>>('~=');
138
+
139
+ assert.deepStrictEqual<readonly number[]>(result, [1, 2, 3]);
140
+ });
141
+
142
+ test('toSortedBy keeps the brand', () => {
143
+ const input: MinLengthArray<3, number> = asMinLengthArray(3, [3, 1, 2]);
144
+
145
+ const result = toSortedBy(input, (x) => x);
146
+
147
+ expectType<typeof result, MinLengthArray<3, number>>('~=');
148
+
149
+ assert.deepStrictEqual<readonly number[]>(result, [1, 2, 3]);
150
+ });
151
+
152
+ test('set keeps the brand and widens only the element type', () => {
153
+ const input: MinLengthArray<3, number> = asMinLengthArray(3, [0, 1, 2]);
154
+
155
+ const result = set(input, 1, 'x');
156
+
157
+ expectType<typeof result, MinLengthArray<3, number | 'x'>>('~=');
158
+
159
+ assert.deepStrictEqual<readonly (number | string)[]>(result, [0, 'x', 2]);
160
+ });
161
+
162
+ test('toUpdated keeps the brand and widens only the element type', () => {
163
+ const input: MinLengthArray<3, number> = asMinLengthArray(3, [0, 1, 2]);
164
+
165
+ const result = toUpdated(input, 1, (prev) => `#${String(prev)}`);
166
+
167
+ expectType<typeof result, MinLengthArray<3, number | `#${string}`>>('~=');
168
+
169
+ assert.deepStrictEqual<readonly (number | string)[]>(result, [0, '#1', 2]);
170
+ });
171
+
172
+ test('toRangeFilled keeps the brand', () => {
173
+ const input: MinLengthArray<3, number> = asMinLengthArray(3, [0, 1, 2]);
174
+
175
+ const result = toRangeFilled(input, 'x', [0, 2]);
176
+
177
+ expectType<typeof result, MinLengthArray<3, number | 'x'>>('~=');
178
+
179
+ assert.deepStrictEqual<readonly (number | string)[]>(result, ['x', 'x', 2]);
180
+ });
181
+
182
+ test('toFilled keeps the brand with the fill type', () => {
183
+ const input: MinLengthArray<3, number> = asMinLengthArray(3, [0, 1, 2]);
184
+
185
+ const result = toFilled(input, 'x');
186
+
187
+ expectType<typeof result, MinLengthArray<3, 'x'>>('~=');
188
+
189
+ assert.deepStrictEqual<readonly string[]>(result, ['x', 'x', 'x']);
190
+ });
191
+
192
+ test('toReversed keeps the brand', () => {
193
+ const input: MinLengthArray<3, number> = asMinLengthArray(3, [1, 2, 3, 4]);
194
+
195
+ const result = toReversed(input);
196
+
197
+ expectType<typeof result, MinLengthArray<3, number>>('~=');
198
+
199
+ // The structural prefix survives the reversal too.
200
+ const first: number = result[0];
201
+
202
+ assert.deepStrictEqual(first, 4);
203
+
204
+ assert.deepStrictEqual<readonly number[]>(result, [4, 3, 2, 1]);
205
+ });
206
+
207
+ test('toReversed still reverses a plain tuple at the type level', () => {
208
+ const result = toReversed([1, 'two', true] as const);
209
+
210
+ expectType<typeof result, readonly [true, 'two', 1]>('=');
211
+
212
+ assert.deepStrictEqual(result, [true, 'two', 1]);
213
+ });
214
+
215
+ test('copy keeps the brand, because it returns the input type', () => {
216
+ const input: FixedLengthArray<3, number> = asFixedLengthArray(3, [1, 2, 3]);
217
+
218
+ const result = copy(input);
219
+
220
+ expectType<typeof result, FixedLengthArray<3, number>>('=');
221
+
222
+ assert.deepStrictEqual<readonly number[]>(result, [1, 2, 3]);
223
+ });
224
+ });
225
+
226
+ describe('partition reports the bounds of each chunk', () => {
227
+ test('a chunk is non-empty and at most the chunk size long', () => {
228
+ const result = partition([1, 2, 3, 4, 5] as const, 2);
229
+
230
+ expectType<
231
+ typeof result,
232
+ readonly BoundedLengthArray<1, 2, 1 | 2 | 3 | 4 | 5>[]
233
+ >('~=');
234
+
235
+ // The lower bound is the point: the first element of a chunk is never
236
+ // `undefined`, unlike with the plain `readonly E[]` this used to return.
237
+ const [firstChunk] = result;
238
+
239
+ // The outer array is unbounded, so this narrows away its `undefined`; the
240
+ // chunk's own first element needs no such check.
241
+ assert.isTrue(firstChunk !== undefined);
242
+
243
+ const first: 1 | 2 | 3 | 4 | 5 = firstChunk[0];
244
+
245
+ assert.deepStrictEqual(first, 1);
246
+
247
+ assert.deepStrictEqual<readonly (readonly number[])[]>(result, [
248
+ [1, 2],
249
+ [3, 4],
250
+ [5],
251
+ ]);
252
+ });
253
+ });
254
+
255
+ describe('creation utilities report an exact length past their tuple range', () => {
256
+ const zeros12 = zeros(12);
257
+
258
+ const seq12 = seq(12);
259
+
260
+ test('create is branded with the exact length', () => {
261
+ const result = create(12, 'x');
262
+
263
+ expectType<typeof result, FixedLengthArray<12, 'x'>>('~=');
264
+
265
+ assert.deepStrictEqual(result.length, 12);
266
+ });
267
+
268
+ test('a length still inside the tuple range keeps its exact tuple type', () => {
269
+ const result = create(3, 'x');
270
+
271
+ expectType<typeof result, FixedLengthTuple<3, 'x'>>('=');
272
+
273
+ assert.deepStrictEqual(result, ['x', 'x', 'x']);
274
+ });
275
+
276
+ test('zeros and seq keep their tuple type across their whole literal range', () => {
277
+ // `SizeType.ArgArr` only carries literals up to `SmallUint`, which these
278
+ // two already expand into a tuple, so there is no branded case to add.
279
+ expectType<typeof zeros12, FixedLengthTuple<12, 0>>('=');
280
+
281
+ expectType<typeof seq12, Seq<12>>('=');
282
+
283
+ assert.deepStrictEqual(zeros12.length, 12);
284
+
285
+ assert.deepStrictEqual(seq12.length, 12);
286
+ });
287
+ });
@@ -1,9 +1,11 @@
1
1
  import {
2
+ type ChangeArrayElement,
3
+ type ConstrainedList,
2
4
  type FixedLengthTuple,
3
5
  type Increment,
4
6
  type IsFixedLengthList,
5
7
  type NonEmptyArray,
6
- type NonEmptyTuple,
8
+ type UnknownBrand,
7
9
  } from 'ts-type-forge';
8
10
  import { asPositiveUint32 } from '../../number/index.mjs';
9
11
  import { castMutable } from '../../others/index.mjs';
@@ -27,27 +29,17 @@ import { copy, create } from './array-utils-creation.mjs';
27
29
  * assert.deepStrictEqual(updated, [10, 25, 30]);
28
30
  * ```
29
31
  */
30
- export function set<const Ar extends readonly unknown[], const V = Ar[number]>(
31
- array: Ar,
32
- index: ArgArrayIndex<Ar>,
33
- newValue: V,
34
- ): IsFixedLengthList<Ar> extends true
35
- ? Readonly<{ [K in keyof Ar]: Ar[K] | V }>
36
- : Ar extends NonEmptyTuple<unknown>
37
- ? NonEmptyArray<Ar[number] | V>
38
- : readonly (Ar[number] | V)[];
32
+ export function set<
33
+ const Ar extends readonly unknown[],
34
+ const I extends ArgArrayIndex<Ar>,
35
+ const V = Ar[number],
36
+ >(array: Ar, index: I, newValue: V): ReplacedAt<Ar, I, V>;
39
37
 
40
38
  // curried version
41
- export function set<const V>(
42
- index: SizeType.ArgArr,
39
+ export function set<const I extends SizeType.ArgArr, const V>(
40
+ index: I,
43
41
  newValue: V,
44
- ): <const Ar extends readonly unknown[]>(
45
- array: Ar,
46
- ) => IsFixedLengthList<Ar> extends true
47
- ? Readonly<{ [K in keyof Ar]: Ar[K] | V }>
48
- : Ar extends NonEmptyTuple<unknown>
49
- ? NonEmptyArray<Ar[number] | V>
50
- : readonly (Ar[number] | V)[];
42
+ ): <const Ar extends readonly unknown[]>(array: Ar) => ReplacedAt<Ar, I, V>;
51
43
 
52
44
  export function set<E, const V = E>(
53
45
  ...args:
@@ -79,7 +71,7 @@ const setImpl = <E, const V = E>(
79
71
  *
80
72
  * const increased = Arr.toUpdated(temperatures, 1, (value) => value + 5);
81
73
  *
82
- * const incrementLast = Arr.toUpdated<number>(
74
+ * const incrementLast = Arr.toUpdated<2, number>(
83
75
  * 2,
84
76
  * (value) => value + 1,
85
77
  * )(temperatures);
@@ -91,28 +83,15 @@ const setImpl = <E, const V = E>(
91
83
  */
92
84
  export function toUpdated<
93
85
  const Ar extends readonly unknown[],
86
+ const I extends ArgArrayIndex<Ar>,
94
87
  const V = Ar[number],
95
- >(
96
- array: Ar,
97
- index: ArgArrayIndex<Ar>,
98
- updater: (prev: Ar[number]) => V,
99
- ): IsFixedLengthList<Ar> extends true
100
- ? Readonly<{ [K in keyof Ar]: Ar[K] | V }>
101
- : Ar extends NonEmptyTuple<unknown>
102
- ? NonEmptyArray<Ar[number] | V>
103
- : readonly (Ar[number] | V)[];
88
+ >(array: Ar, index: I, updater: (prev: Ar[number]) => V): ReplacedAt<Ar, I, V>;
104
89
 
105
90
  // curried version
106
- export function toUpdated<E, const V = E>(
107
- index: SizeType.ArgArr,
91
+ export function toUpdated<const I extends SizeType.ArgArr, E, const V = E>(
92
+ index: I,
108
93
  updater: (prev: E) => V,
109
- ): <const Ar extends readonly E[]>(
110
- array: Ar,
111
- ) => IsFixedLengthList<Ar> extends true
112
- ? Readonly<{ [K in keyof Ar]: Ar[K] | V }>
113
- : Ar extends NonEmptyTuple<unknown>
114
- ? NonEmptyArray<Ar[number] | V>
115
- : readonly (Ar[number] | V)[];
94
+ ): <const Ar extends readonly E[]>(array: Ar) => ReplacedAt<Ar, I, V>;
116
95
 
117
96
  export function toUpdated<E, V = E>(
118
97
  ...args:
@@ -376,25 +355,33 @@ const toUnshiftedImpl = <Ar extends readonly unknown[], const V>(
376
355
  * assert.deepStrictEqual(filledCurried, ['x', 'x', 'x']);
377
356
  * ```
378
357
  */
358
+ export function toFilled<
359
+ const Ar extends readonly unknown[] & UnknownBrand,
360
+ const V,
361
+ >(array: Ar, value: V): ChangeArrayElement<Ar, V>;
362
+
363
+ // For an array or tuple that carries no length brand the homomorphic mapping
364
+ // already covers every case the conditional spelled out — a fixed-length tuple
365
+ // keeps its length, `readonly [A, ...A[]]` stays non-empty, and `readonly A[]`
366
+ // stays unbounded. Stating it directly is what keeps this usable from a
367
+ // caller that is itself generic over the array: a conditional a *generic* `Ar`
368
+ // cannot decide stays deferred, and its branded branch is then not assignable
369
+ // to the caller's own mapping.
379
370
  export function toFilled<const Ar extends readonly unknown[], const V>(
380
371
  array: Ar,
381
372
  value: V,
382
- ): IsFixedLengthList<Ar> extends true
383
- ? FixedLengthTuple<Ar['length'], V>
384
- : Ar extends NonEmptyTuple<unknown>
385
- ? NonEmptyArray<V>
386
- : readonly V[];
373
+ ): Readonly<{ [K in keyof Ar]: V }>;
387
374
 
388
- // curried version
375
+ // curried version — the two cases have to be overloads of the *returned*
376
+ // function, spelled as an intersection of function types (see `Arr.map`).
389
377
  export function toFilled<const V>(
390
378
  value: V,
391
- ): <const Ar extends readonly unknown[]>(
379
+ ): (<const Ar extends readonly unknown[] & UnknownBrand>(
392
380
  array: Ar,
393
- ) => IsFixedLengthList<Ar> extends true
394
- ? FixedLengthTuple<Ar['length'], V>
395
- : Ar extends NonEmptyTuple<unknown>
396
- ? NonEmptyArray<V>
397
- : readonly V[];
381
+ ) => ChangeArrayElement<Ar, V>) &
382
+ (<const Ar extends readonly unknown[]>(
383
+ array: Ar,
384
+ ) => Readonly<{ [K in keyof Ar]: V }>);
398
385
 
399
386
  export function toFilled<E>(
400
387
  ...args: readonly [array: readonly E[], value: E] | readonly [value: E]
@@ -428,6 +415,20 @@ const toFilledImpl = <E,>(array: readonly E[], value: E): readonly E[] =>
428
415
  * assert.deepStrictEqual(filledPrefix, [8, 8, 2, 3, 4]);
429
416
  * ```
430
417
  */
418
+ export function toRangeFilled<
419
+ const Ar extends readonly unknown[] & UnknownBrand,
420
+ const V,
421
+ >(
422
+ array: Ar,
423
+ value: V,
424
+ fillRange: readonly [
425
+ start: ArgArrayIndexWithNegative<Ar>,
426
+ end: ArgArrayIndexWithNegative<Ar>,
427
+ ],
428
+ ): ChangeArrayElement<Ar, V | Ar[number]>;
429
+
430
+ // See `toFilled` for why the unbranded case states the homomorphic mapping
431
+ // directly instead of going through a conditional.
431
432
  export function toRangeFilled<const Ar extends readonly unknown[], const V>(
432
433
  array: Ar,
433
434
  value: V,
@@ -435,11 +436,7 @@ export function toRangeFilled<const Ar extends readonly unknown[], const V>(
435
436
  start: ArgArrayIndexWithNegative<Ar>,
436
437
  end: ArgArrayIndexWithNegative<Ar>,
437
438
  ],
438
- ): IsFixedLengthList<Ar> extends true
439
- ? FixedLengthTuple<Ar['length'], V | Ar[number]>
440
- : Ar extends NonEmptyTuple<unknown>
441
- ? NonEmptyArray<V | Ar[number]>
442
- : readonly (V | Ar[number])[];
439
+ ): Readonly<{ [K in keyof Ar]: V | Ar[number] }>;
443
440
 
444
441
  // curried version
445
442
  export function toRangeFilled<const V>(
@@ -448,13 +445,12 @@ export function toRangeFilled<const V>(
448
445
  start: SizeType.ArgArrWithNegative,
449
446
  end: SizeType.ArgArrWithNegative,
450
447
  ],
451
- ): <const Ar extends readonly unknown[]>(
448
+ ): (<const Ar extends readonly unknown[] & UnknownBrand>(
452
449
  array: Ar,
453
- ) => IsFixedLengthList<Ar> extends true
454
- ? FixedLengthTuple<Ar['length'], V | Ar[number]>
455
- : Ar extends NonEmptyTuple<unknown>
456
- ? NonEmptyArray<V | Ar[number]>
457
- : readonly (V | Ar[number])[];
450
+ ) => ChangeArrayElement<Ar, V | Ar[number]>) &
451
+ (<const Ar extends readonly unknown[]>(
452
+ array: Ar,
453
+ ) => Readonly<{ [K in keyof Ar]: V | Ar[number] }>);
458
454
 
459
455
  export function toRangeFilled<E, const V>(
460
456
  ...args:
@@ -499,3 +495,30 @@ const toRangeFilledImpl = <E, const V>(
499
495
 
500
496
  return mut_cp;
501
497
  };
498
+
499
+ /**
500
+ * The result of replacing the element at `I` of `Ar` with a `V`.
501
+ *
502
+ * `ConstrainedList.SetAt` replaces exactly one position when the index pins
503
+ * one, which is what makes `Arr.set(xs, 1, v)` report `readonly [a, V, c]`
504
+ * instead of widening every position to `a | V`.
505
+ *
506
+ * A union index needs the weaker answer, and this used to guard for it here:
507
+ * `ArgArrayIndex<Ar>` for a tuple is the union of its indices, so
508
+ * `Arr.set(tuple, someIndex, v)` with `someIndex: 0 | 2` binds `I = 0 | 2`, and
509
+ * the upstream `List.SetAt` used to answer `readonly [V, b, V]` — a type that
510
+ * *neither* possible runtime result satisfies, since the call replaces one
511
+ * position or the other and never both.
512
+ *
513
+ * ts-type-forge 9.1.1 fixes that at the source and widens the candidate
514
+ * positions itself, so the guard is gone. Upstream is the more precise of the
515
+ * two: it widens only the positions the index can name, where the guard here
516
+ * widened all of them — `Arr.set(t, i, 'x')` with `i: 0 | 2` on
517
+ * `readonly [1, 2, 3]` now keeps the middle element at `2` rather than
518
+ * reporting `2 | 'x'`.
519
+ */
520
+ type ReplacedAt<
521
+ Ar extends readonly unknown[],
522
+ I extends number,
523
+ V,
524
+ > = ConstrainedList.SetAt<I, V, Ar>;