ts-data-forge 1.0.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.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +534 -0
  3. package/package.json +101 -0
  4. package/src/array/array-utils-creation.test.mts +443 -0
  5. package/src/array/array-utils-modification.test.mts +197 -0
  6. package/src/array/array-utils-overload-type-error.test.mts +149 -0
  7. package/src/array/array-utils-reducing-value.test.mts +425 -0
  8. package/src/array/array-utils-search.test.mts +169 -0
  9. package/src/array/array-utils-set-op.test.mts +335 -0
  10. package/src/array/array-utils-slice-clamped.test.mts +113 -0
  11. package/src/array/array-utils-slicing.test.mts +316 -0
  12. package/src/array/array-utils-transformation.test.mts +790 -0
  13. package/src/array/array-utils-validation.test.mts +492 -0
  14. package/src/array/array-utils.mts +4000 -0
  15. package/src/array/array.test.mts +146 -0
  16. package/src/array/index.mts +2 -0
  17. package/src/array/tuple-utils.mts +519 -0
  18. package/src/array/tuple-utils.test.mts +518 -0
  19. package/src/collections/imap-mapped.mts +801 -0
  20. package/src/collections/imap-mapped.test.mts +860 -0
  21. package/src/collections/imap.mts +651 -0
  22. package/src/collections/imap.test.mts +932 -0
  23. package/src/collections/index.mts +6 -0
  24. package/src/collections/iset-mapped.mts +889 -0
  25. package/src/collections/iset-mapped.test.mts +1187 -0
  26. package/src/collections/iset.mts +682 -0
  27. package/src/collections/iset.test.mts +1084 -0
  28. package/src/collections/queue.mts +390 -0
  29. package/src/collections/queue.test.mts +282 -0
  30. package/src/collections/stack.mts +423 -0
  31. package/src/collections/stack.test.mts +225 -0
  32. package/src/expect-type.mts +206 -0
  33. package/src/functional/index.mts +4 -0
  34. package/src/functional/match.mts +300 -0
  35. package/src/functional/match.test.mts +177 -0
  36. package/src/functional/optional.mts +733 -0
  37. package/src/functional/optional.test.mts +619 -0
  38. package/src/functional/pipe.mts +212 -0
  39. package/src/functional/pipe.test.mts +85 -0
  40. package/src/functional/result.mts +1134 -0
  41. package/src/functional/result.test.mts +777 -0
  42. package/src/globals.d.mts +38 -0
  43. package/src/guard/has-key.mts +119 -0
  44. package/src/guard/has-key.test.mts +219 -0
  45. package/src/guard/index.mts +7 -0
  46. package/src/guard/is-non-empty-string.mts +108 -0
  47. package/src/guard/is-non-empty-string.test.mts +91 -0
  48. package/src/guard/is-non-null-object.mts +106 -0
  49. package/src/guard/is-non-null-object.test.mts +90 -0
  50. package/src/guard/is-primitive.mts +165 -0
  51. package/src/guard/is-primitive.test.mts +102 -0
  52. package/src/guard/is-record.mts +153 -0
  53. package/src/guard/is-record.test.mts +112 -0
  54. package/src/guard/is-type.mts +450 -0
  55. package/src/guard/is-type.test.mts +496 -0
  56. package/src/guard/key-is-in.mts +163 -0
  57. package/src/guard/key-is-in.test.mts +19 -0
  58. package/src/index.mts +10 -0
  59. package/src/iterator/index.mts +1 -0
  60. package/src/iterator/range.mts +120 -0
  61. package/src/iterator/range.test.mts +33 -0
  62. package/src/json/index.mts +1 -0
  63. package/src/json/json.mts +711 -0
  64. package/src/json/json.test.mts +628 -0
  65. package/src/number/branded-types/finite-number.mts +354 -0
  66. package/src/number/branded-types/finite-number.test.mts +135 -0
  67. package/src/number/branded-types/index.mts +26 -0
  68. package/src/number/branded-types/int.mts +278 -0
  69. package/src/number/branded-types/int.test.mts +140 -0
  70. package/src/number/branded-types/int16.mts +192 -0
  71. package/src/number/branded-types/int16.test.mts +170 -0
  72. package/src/number/branded-types/int32.mts +193 -0
  73. package/src/number/branded-types/int32.test.mts +170 -0
  74. package/src/number/branded-types/non-negative-finite-number.mts +223 -0
  75. package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
  76. package/src/number/branded-types/non-negative-int16.mts +187 -0
  77. package/src/number/branded-types/non-negative-int16.test.mts +201 -0
  78. package/src/number/branded-types/non-negative-int32.mts +187 -0
  79. package/src/number/branded-types/non-negative-int32.test.mts +204 -0
  80. package/src/number/branded-types/non-zero-finite-number.mts +229 -0
  81. package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
  82. package/src/number/branded-types/non-zero-int.mts +167 -0
  83. package/src/number/branded-types/non-zero-int.test.mts +177 -0
  84. package/src/number/branded-types/non-zero-int16.mts +196 -0
  85. package/src/number/branded-types/non-zero-int16.test.mts +195 -0
  86. package/src/number/branded-types/non-zero-int32.mts +196 -0
  87. package/src/number/branded-types/non-zero-int32.test.mts +197 -0
  88. package/src/number/branded-types/non-zero-safe-int.mts +196 -0
  89. package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
  90. package/src/number/branded-types/non-zero-uint16.mts +189 -0
  91. package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
  92. package/src/number/branded-types/non-zero-uint32.mts +189 -0
  93. package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
  94. package/src/number/branded-types/positive-finite-number.mts +241 -0
  95. package/src/number/branded-types/positive-finite-number.test.mts +204 -0
  96. package/src/number/branded-types/positive-int.mts +304 -0
  97. package/src/number/branded-types/positive-int.test.mts +176 -0
  98. package/src/number/branded-types/positive-int16.mts +188 -0
  99. package/src/number/branded-types/positive-int16.test.mts +197 -0
  100. package/src/number/branded-types/positive-int32.mts +188 -0
  101. package/src/number/branded-types/positive-int32.test.mts +197 -0
  102. package/src/number/branded-types/positive-safe-int.mts +187 -0
  103. package/src/number/branded-types/positive-safe-int.test.mts +210 -0
  104. package/src/number/branded-types/positive-uint16.mts +188 -0
  105. package/src/number/branded-types/positive-uint16.test.mts +203 -0
  106. package/src/number/branded-types/positive-uint32.mts +188 -0
  107. package/src/number/branded-types/positive-uint32.test.mts +203 -0
  108. package/src/number/branded-types/safe-int.mts +291 -0
  109. package/src/number/branded-types/safe-int.test.mts +170 -0
  110. package/src/number/branded-types/safe-uint.mts +187 -0
  111. package/src/number/branded-types/safe-uint.test.mts +176 -0
  112. package/src/number/branded-types/uint.mts +179 -0
  113. package/src/number/branded-types/uint.test.mts +158 -0
  114. package/src/number/branded-types/uint16.mts +186 -0
  115. package/src/number/branded-types/uint16.test.mts +170 -0
  116. package/src/number/branded-types/uint32.mts +218 -0
  117. package/src/number/branded-types/uint32.test.mts +170 -0
  118. package/src/number/enum/index.mts +2 -0
  119. package/src/number/enum/int8.mts +344 -0
  120. package/src/number/enum/int8.test.mts +180 -0
  121. package/src/number/enum/uint8.mts +293 -0
  122. package/src/number/enum/uint8.test.mts +164 -0
  123. package/src/number/index.mts +4 -0
  124. package/src/number/num.mts +604 -0
  125. package/src/number/num.test.mts +242 -0
  126. package/src/number/refined-number-utils.mts +566 -0
  127. package/src/object/index.mts +1 -0
  128. package/src/object/object.mts +447 -0
  129. package/src/object/object.test.mts +124 -0
  130. package/src/others/cast-mutable.mts +113 -0
  131. package/src/others/cast-readonly.mts +192 -0
  132. package/src/others/cast-readonly.test.mts +89 -0
  133. package/src/others/if-then.mts +98 -0
  134. package/src/others/if-then.test.mts +75 -0
  135. package/src/others/index.mts +7 -0
  136. package/src/others/map-nullable.mts +172 -0
  137. package/src/others/map-nullable.test.mts +297 -0
  138. package/src/others/memoize-function.mts +196 -0
  139. package/src/others/memoize-function.test.mts +168 -0
  140. package/src/others/tuple.mts +160 -0
  141. package/src/others/tuple.test.mts +11 -0
  142. package/src/others/unknown-to-string.mts +215 -0
  143. package/src/others/unknown-to-string.test.mts +114 -0
@@ -0,0 +1,146 @@
1
+ import { expectType } from '../expect-type.mjs';
2
+
3
+ describe('Array.every', () => {
4
+ const xs = [1, 2, 3] as const;
5
+
6
+ if (xs.every((x): x is 1 => x === 1)) {
7
+ expectType<typeof xs, readonly [1, 2, 3] & readonly 1[]>('=');
8
+ } else {
9
+ expectType<typeof xs, readonly [1, 2, 3]>('=');
10
+ }
11
+
12
+ test('case 1', () => {
13
+ expect(xs.every((x): x is 1 => x === 1)).toBe(false);
14
+ });
15
+
16
+ test('case 2', () => {
17
+ expect(xs.every((x) => 1 <= x && x <= 3)).toBe(true);
18
+ });
19
+ });
20
+
21
+ describe('Array.some', () => {
22
+ const xs = [1, 2, 3] as const;
23
+
24
+ test('case 1', () => {
25
+ expect(xs.some((x): x is 1 => x === 1)).toBe(true);
26
+ });
27
+
28
+ test('case 2', () => {
29
+ expect(xs.some((x) => x <= 1 && 3 <= x)).toBe(false);
30
+ });
31
+ });
32
+
33
+ describe('Array.flat', () => {
34
+ const xs = [1, 2, [3, 4, [5, 6, [7, 8]]]] as const;
35
+ const result = xs.flat(1);
36
+
37
+ expectType<
38
+ typeof result,
39
+ (readonly [5, 6, readonly [7, 8]] | 1 | 2 | 3 | 4)[]
40
+ >('=');
41
+
42
+ test('case 1', () => {
43
+ expect(result).toStrictEqual([1, 2, 3, 4, [5, 6, [7, 8]]]);
44
+ });
45
+ });
46
+
47
+ describe('Array.includes', () => {
48
+ {
49
+ const xs = [2, 1, 3] as const;
50
+ const result = xs.includes(2);
51
+
52
+ expectType<typeof result, boolean>('=');
53
+
54
+ test('case 1', () => {
55
+ expect(result).toBe(true);
56
+ });
57
+ }
58
+ {
59
+ const xs: readonly number[] = [2, 1, 3];
60
+ const result = xs.includes(4);
61
+
62
+ expectType<typeof result, boolean>('=');
63
+
64
+ test('case 2', () => {
65
+ expect(result).toBe(false);
66
+ });
67
+ }
68
+ });
69
+
70
+ describe('Array.find', () => {
71
+ {
72
+ const xs = [{ v: 2 }, { v: 1 }, { v: 3 }] as const;
73
+ const result = xs.find((x) => x.v === 1);
74
+
75
+ expectType<typeof result, Readonly<{ v: 1 }> | undefined>('=');
76
+
77
+ test('case 1', () => {
78
+ expect(result).toStrictEqual({ v: 1 });
79
+ });
80
+ }
81
+ {
82
+ const xs: readonly Readonly<{ v: 1 | 2 | 3 }>[] = [
83
+ { v: 2 },
84
+ { v: 1 },
85
+ { v: 3 },
86
+ ] as const;
87
+ const result = xs.find((x) => x.v === 1);
88
+
89
+ expectType<typeof result, Readonly<{ v: 1 | 2 | 3 }> | undefined>('=');
90
+
91
+ test('case 2', () => {
92
+ expect(result).toStrictEqual({ v: 1 });
93
+ });
94
+ }
95
+ });
96
+
97
+ describe('Array.findIndex', () => {
98
+ {
99
+ const xs = [{ v: 2 }, { v: 1 }, { v: 3 }] as const;
100
+ const result = xs.findIndex((x) => x.v === 1);
101
+
102
+ expectType<typeof result, number>('=');
103
+
104
+ test('case 1', () => {
105
+ expect(result).toBe(1);
106
+ });
107
+ }
108
+ {
109
+ const xs: readonly Readonly<{ v: 1 | 2 | 3 }>[] = [
110
+ { v: 2 },
111
+ { v: 1 },
112
+ { v: 3 },
113
+ ] as const;
114
+ const result = xs.findIndex((x) => x.v === 1);
115
+
116
+ expectType<typeof result, number>('=');
117
+
118
+ test('case 2', () => {
119
+ expect(result).toBe(1);
120
+ });
121
+ }
122
+ });
123
+
124
+ describe('Array.filter', () => {
125
+ {
126
+ const xs = [1, 2, 3] as const;
127
+ const filtered = xs.filter((x): x is 1 | 3 => x % 2 === 1);
128
+
129
+ expectType<typeof filtered, (1 | 3)[]>('=');
130
+
131
+ test('case 1', () => {
132
+ expect(filtered).toStrictEqual([1, 3]);
133
+ });
134
+ }
135
+
136
+ {
137
+ const xs = [1, 2, 3] as const;
138
+ const filtered = xs.filter((x) => x % 2 === 1);
139
+
140
+ expectType<typeof filtered, (1 | 2 | 3)[]>('=');
141
+
142
+ test('case 2', () => {
143
+ expect(filtered).toStrictEqual([1, 3]);
144
+ });
145
+ }
146
+ });
@@ -0,0 +1,2 @@
1
+ export * from './array-utils.mjs';
2
+ export * from './tuple-utils.mjs';
@@ -0,0 +1,519 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
2
+ /**
3
+ * A collection of tuple utility functions.
4
+ *
5
+ * Provides type-safe operations for working with tuples (fixed-length arrays).
6
+ * Unlike regular arrays, tuples preserve their exact length and element types
7
+ * at compile time, enabling precise type inference and validation.
8
+ *
9
+ * Key features:
10
+ * - All operations preserve tuple length and element types
11
+ * - Type-safe indexing with compile-time bounds checking
12
+ * - Immutable operations that return new tuples
13
+ * - Precise type inference for transformed elements
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Tuples preserve exact types and length
18
+ * const tuple = [1, 'hello', true] as const;
19
+ * type TupleType = typeof tuple; // readonly [1, 'hello', true]
20
+ *
21
+ * // Operations preserve tuple structure
22
+ * const mapped = Tpl.map(tuple, (x) => String(x)); // readonly ['1', 'hello', 'true']
23
+ * const reversed = Tpl.toReversed(tuple); // readonly [true, 'hello', 1]
24
+ * ```
25
+ */
26
+ export namespace Tpl {
27
+ // length: size,
28
+
29
+ /**
30
+ * Returns the length of a tuple as a literal type.
31
+ *
32
+ * Unlike `array.length` which returns `number`, this preserves
33
+ * the exact length as a literal type (e.g., `3` not `number`).
34
+ *
35
+ * @template T - The tuple type whose length will be extracted
36
+ * @param list - The input tuple
37
+ * @returns The length of the tuple as a literal number type
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const tpl = [1, 2, 3] as const;
42
+ * const len = Tpl.size(tpl); // 3 (literal type, not just number)
43
+ *
44
+ * // Type-level length extraction
45
+ * type Len = Length<typeof tpl>; // 3
46
+ *
47
+ * // Useful for compile-time validation
48
+ * function requiresTriple<T extends readonly [unknown, unknown, unknown]>(t: T) {}
49
+ * const pair = [1, 2] as const;
50
+ * // requiresTriple(pair); // Error: length mismatch
51
+ * ```
52
+ */
53
+ export const size = <const T extends readonly unknown[]>(
54
+ list: T,
55
+ ): Length<T> => list.length;
56
+
57
+ export const length = size;
58
+
59
+ /**
60
+ * Helper type to refine the result of array search methods.
61
+ *
62
+ * Prevents overly specific number literal types when the search might not find an element.
63
+ * - If T is exactly `number`, it remains `number`
64
+ * - Otherwise, T is returned as-is (preserving literal types)
65
+ *
66
+ * This ensures that index types are practical while maintaining type safety.
67
+ *
68
+ * @template T - The type to map
69
+ * @internal
70
+ */
71
+ type MapNumberToArraySearchResult<T> = T extends number
72
+ ? TypeEq<T, number> extends true
73
+ ? number // If T is the general number type, keep it as number
74
+ : T // Otherwise (e.g., a number literal), keep the specific type
75
+ : T;
76
+
77
+ /**
78
+ * Refines the `IndexOfTuple` type using `MapNumberToArraySearchResult`.
79
+ *
80
+ * Provides accurate types for indices found in a tuple, balancing between
81
+ * type precision and practicality in handling search results.
82
+ *
83
+ * @template T - The tuple type whose indices are being refined
84
+ * @internal
85
+ */
86
+ type IndexOfTupleRefined<T extends readonly unknown[]> =
87
+ MapNumberToArraySearchResult<IndexOfTuple<T>>;
88
+
89
+ /**
90
+ * Finds the index of the first element in a tuple that satisfies the predicate.
91
+ *
92
+ * Returns a type-safe index that can be one of the valid tuple indices or -1.
93
+ * The return type is precisely inferred based on the tuple's length.
94
+ *
95
+ * @template T - The tuple type to search within
96
+ * @param tpl - The input tuple
97
+ * @param predicate - A function to test each element for a condition
98
+ * @returns The index of the first matching element, or -1 if not found
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const tpl = [1, 2, 3, 4] as const;
103
+ * const idx1 = Tpl.findIndex(tpl, (x) => x > 2); // 2 | 3 | -1
104
+ * const idx2 = Tpl.findIndex(tpl, (x) => x > 10); // -1
105
+ *
106
+ * // Type-safe indexing
107
+ * if (idx1 !== -1) {
108
+ * const value = tpl[idx1]; // Safe access, TypeScript knows idx1 is valid
109
+ * }
110
+ *
111
+ * // With mixed types
112
+ * const mixed = [1, 'hello', true, null] as const;
113
+ * const strIndex = Tpl.findIndex(mixed, (x) => typeof x === 'string'); // 1 | -1
114
+ * ```
115
+ */
116
+ export const findIndex = <const T extends readonly unknown[]>(
117
+ tpl: T,
118
+ predicate: (value: T[number], index: SizeType.Arr) => boolean,
119
+ ): IndexOfTupleRefined<T> | -1 =>
120
+ tpl.findIndex((value, index) => predicate(value, index as SizeType.Arr)) as
121
+ | IndexOfTupleRefined<T>
122
+ | -1;
123
+
124
+ /**
125
+ * Returns the first index at which a given element can be found in the tuple.
126
+ *
127
+ * Performs strict equality checking (===) and returns a type-safe index.
128
+ * The return type precisely reflects the possible indices of the tuple.
129
+ *
130
+ * @template T - The tuple type to search within
131
+ * @param tpl - The input tuple
132
+ * @param searchElement - Element to locate in the tuple (must be assignable to tuple's element types)
133
+ * @param fromIndex - Optional index to start the search at (defaults to 0)
134
+ * @returns The first index of the element, or -1 if not found
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const tpl = ['a', 'b', 'c', 'b'] as const;
139
+ * const idx1 = Tpl.indexOf(tpl, 'b'); // 1 | 3 | -1 (type shows possible indices)
140
+ * const idx2 = Tpl.indexOf(tpl, 'b', 2); // 3 | -1 (search from index 2)
141
+ * const idx3 = Tpl.indexOf(tpl, 'd'); // -1
142
+ *
143
+ * // Type safety with literal types
144
+ * const nums = [1, 2, 3, 2] as const;
145
+ * const twoIndex = Tpl.indexOf(nums, 2); // 1 | 3 | -1
146
+ * // Tpl.indexOf(nums, '2'); // Error: string not assignable to 1 | 2 | 3
147
+ *
148
+ * // Works with mixed types
149
+ * const mixed = [1, 'hello', true, 1] as const;
150
+ * const oneIndex = Tpl.indexOf(mixed, 1); // 0 | 3 | -1
151
+ * ```
152
+ */
153
+ export const indexOf = <const T extends readonly unknown[]>(
154
+ tpl: T,
155
+ searchElement: T[number],
156
+ fromIndex?: IndexOfTupleRefined<T>,
157
+ ): IndexOfTupleRefined<T> | -1 =>
158
+ tpl.indexOf(searchElement, fromIndex) as
159
+ | MapNumberToArraySearchResult<IndexOfTuple<T>>
160
+ | -1;
161
+
162
+ /**
163
+ * Returns the last index at which a given element can be found in the tuple.
164
+ *
165
+ * Searches backwards from the end (or from `fromIndex` if provided) and
166
+ * returns a type-safe index reflecting the tuple's structure.
167
+ *
168
+ * @template T - The tuple type to search within
169
+ * @param tpl - The input tuple
170
+ * @param searchElement - Element to locate in the tuple
171
+ * @param fromIndex - Optional index to start searching backwards from (defaults to last index)
172
+ * @returns The last index of the element, or -1 if not found
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * const tpl = ['a', 'b', 'c', 'b'] as const;
177
+ * const idx1 = Tpl.lastIndexOf(tpl, 'b'); // 3 | 1 | -1
178
+ * const idx2 = Tpl.lastIndexOf(tpl, 'b', 2); // 1 | -1 (search up to index 2)
179
+ * const idx3 = Tpl.lastIndexOf(tpl, 'd'); // -1
180
+ *
181
+ * // With duplicate values
182
+ * const nums = [1, 2, 3, 2, 1] as const;
183
+ * const lastOne = Tpl.lastIndexOf(nums, 1); // 4 | 0 | -1
184
+ * const lastTwo = Tpl.lastIndexOf(nums, 2); // 3 | 1 | -1
185
+ *
186
+ * // Type safety preserved
187
+ * const mixed = [true, 42, 'str', 42] as const;
188
+ * const last42 = Tpl.lastIndexOf(mixed, 42); // 3 | 1 | -1
189
+ * // Tpl.lastIndexOf(mixed, false); // Error: false not in tuple type
190
+ * ```
191
+ */
192
+ export const lastIndexOf = <const T extends readonly unknown[]>(
193
+ tpl: T,
194
+ searchElement: T[number],
195
+ fromIndex?: IndexOfTupleRefined<T>,
196
+ ): IndexOfTupleRefined<T> | -1 =>
197
+ tpl.lastIndexOf(searchElement, fromIndex) as IndexOfTupleRefined<T> | -1;
198
+
199
+ /**
200
+ * Creates a new tuple by transforming each element with a mapping function.
201
+ *
202
+ * Preserves the tuple's length while allowing element type transformation.
203
+ * The resulting tuple has the same structure but with transformed element types.
204
+ *
205
+ * @template T - The type of the input tuple
206
+ * @template B - The type that elements will be transformed to
207
+ * @param tpl - The input tuple
208
+ * @param mapFn - Function that transforms each element (receives element and index)
209
+ * @returns A new tuple with transformed elements, preserving the original length
210
+ *
211
+ * @example
212
+ * ```typescript
213
+ * // Basic transformation
214
+ * const nums = [1, 2, 3] as const;
215
+ * const doubled = Tpl.map(nums, (x) => x * 2); // readonly [2, 4, 6]
216
+ * const strings = Tpl.map(nums, (x) => String(x)); // readonly ['1', '2', '3']
217
+ *
218
+ * // With index
219
+ * const indexed = Tpl.map(nums, (x, i) => `${i}:${x}`);
220
+ * // readonly ['0:1', '1:2', '2:3']
221
+ *
222
+ * // Mixed type tuples
223
+ * const mixed = [1, 'hello', true] as const;
224
+ * const descriptions = Tpl.map(mixed, (x) => `Value: ${x}`);
225
+ * // readonly ['Value: 1', 'Value: hello', 'Value: true']
226
+ *
227
+ * // Type transformation preserves tuple structure
228
+ * type Original = readonly [number, string, boolean];
229
+ * type Mapped = { readonly [K in keyof Original]: string };
230
+ * // Mapped = readonly [string, string, string]
231
+ * ```
232
+ */
233
+ export const map = <const T extends readonly unknown[], const B>(
234
+ tpl: T,
235
+ mapFn: (a: T[number], index: SizeType.Arr) => B,
236
+ ): { readonly [K in keyof T]: B } =>
237
+ tpl.map((a, index) => mapFn(a as T[number], index as SizeType.Arr)) as {
238
+ readonly [K in keyof T]: B;
239
+ };
240
+
241
+ // modification (returns new array)
242
+
243
+ /**
244
+ * Returns a new tuple with the element at the specified index replaced.
245
+ *
246
+ * This operation is type-safe with compile-time index validation.
247
+ * The resulting tuple type reflects that the element at the given index
248
+ * may be either the new type or the original type.
249
+ *
250
+ * @template T - The type of the input tuple
251
+ * @template N - The type of the new value to set
252
+ * @param tpl - The input tuple
253
+ * @param index - The index to update (must be valid for the tuple length)
254
+ * @param newValue - The new value to place at the index
255
+ * @returns A new tuple with the updated element
256
+ *
257
+ * @example
258
+ * ```typescript
259
+ * // Basic usage
260
+ * const tpl = ['a', 'b', 'c'] as const;
261
+ * const updated = Tpl.set(tpl, 1, 'B'); // readonly ['a', 'B', 'c']
262
+ *
263
+ * // Type changes are reflected
264
+ * const mixed = [1, 'hello', true] as const;
265
+ * const withNumber = Tpl.set(mixed, 1, 42);
266
+ * // readonly [1, 42 | 'hello', true]
267
+ *
268
+ * // Compile-time index validation
269
+ * const short = [1, 2] as const;
270
+ * // Tpl.set(short, 2, 3); // Error: index 2 is out of bounds
271
+ *
272
+ * // Different value types
273
+ * const nums = [1, 2, 3] as const;
274
+ * const withString = Tpl.set(nums, 0, 'first');
275
+ * // readonly ['first' | 1, 2, 3]
276
+ * ```
277
+ */
278
+ export const set = <const T extends readonly unknown[], const N>(
279
+ tpl: T,
280
+ index: Index<Length<T>>,
281
+ newValue: N,
282
+ ): { readonly [K in keyof T]: N | T[K] } =>
283
+ map(tpl, (a, i) => (i === index ? newValue : a)) as {
284
+ readonly [K in keyof T]: N | T[K];
285
+ };
286
+
287
+ /**
288
+ * Returns a new tuple with an element updated by applying a function.
289
+ *
290
+ * Similar to `set`, but instead of providing a new value directly,
291
+ * you provide a function that transforms the existing value.
292
+ * Useful for computed updates based on the current value.
293
+ *
294
+ * @template T - The type of the input tuple
295
+ * @template N - The type returned by the updater function
296
+ * @param tpl - The input tuple
297
+ * @param index - The index of the element to update
298
+ * @param updater - Function that transforms the current value to a new value
299
+ * @returns A new tuple with the updated element
300
+ *
301
+ * @example
302
+ * ```typescript
303
+ * // Numeric updates
304
+ * const nums = [1, 2, 3] as const;
305
+ * const doubled = Tpl.toUpdated(nums, 1, (x) => x * 10);
306
+ * // readonly [1, 20, 3]
307
+ *
308
+ * // String transformations
309
+ * const strs = ['hello', 'world', '!'] as const;
310
+ * const uppercased = Tpl.toUpdated(strs, 0, (s) => s.toUpperCase());
311
+ * // readonly ['HELLO', 'world', '!']
312
+ *
313
+ * // Complex transformations
314
+ * const data = [{count: 1}, {count: 2}, {count: 3}] as const;
315
+ * const incremented = Tpl.toUpdated(data, 1, (obj) => ({count: obj.count + 1}));
316
+ * // Updates the second object's count to 3
317
+ *
318
+ * // Type changes through updater
319
+ * const mixed = [1, 'hello', true] as const;
320
+ * const stringified = Tpl.toUpdated(mixed, 0, (n) => `Number: ${n}`);
321
+ * // readonly ['Number: 1' | 1, 'hello', true]
322
+ * ```
323
+ */
324
+ export const toUpdated = <const T extends readonly unknown[], const N>(
325
+ tpl: T,
326
+ index: SizeType.ArgArrNonNegative | (Index<Length<T>> & SmallUint),
327
+ updater: (prev: T[number]) => N,
328
+ ): { readonly [K in keyof T]: N | T[K] } =>
329
+ map(tpl, (a, i) => (i === index ? updater(a) : a)) as {
330
+ readonly [K in keyof T]: N | T[K];
331
+ };
332
+
333
+ // transformation
334
+
335
+ /**
336
+ * Reverses a tuple, preserving element types in their new positions.
337
+ *
338
+ * The type system precisely tracks the reversal, so the returned tuple
339
+ * has its element types in the exact reverse order. This is more precise
340
+ * than array reversal which loses positional type information.
341
+ *
342
+ * @template T - The tuple type to reverse
343
+ * @param tpl - The input tuple
344
+ * @returns A new tuple with elements in reverse order and precise typing
345
+ *
346
+ * @example
347
+ * ```typescript
348
+ * // Basic reversal
349
+ * const nums = [1, 2, 3] as const;
350
+ * const reversed = Tpl.toReversed(nums); // readonly [3, 2, 1]
351
+ *
352
+ * // Mixed types preserved in reverse positions
353
+ * const mixed = [1, 'hello', true, null] as const;
354
+ * const revMixed = Tpl.toReversed(mixed);
355
+ * // readonly [null, true, 'hello', 1]
356
+ *
357
+ * // Type-level reversal
358
+ * type Original = readonly [number, string, boolean];
359
+ * type Reversed = Tuple.Reverse<Original>;
360
+ * // Reversed = readonly [boolean, string, number]
361
+ *
362
+ * // Empty and single-element tuples
363
+ * const empty = [] as const;
364
+ * const revEmpty = Tpl.toReversed(empty); // readonly []
365
+ * const single = [42] as const;
366
+ * const revSingle = Tpl.toReversed(single); // readonly [42]
367
+ * ```
368
+ */
369
+ export const toReversed = <const T extends readonly unknown[]>(
370
+ tpl: T,
371
+ ): Tuple.Reverse<T> =>
372
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
373
+ tpl.toReversed() as Tuple.Reverse<T>;
374
+
375
+ /**
376
+ * Sorts a tuple's elements, returning a new tuple with the same length.
377
+ *
378
+ * Unlike array sorting, this preserves the tuple's length but loses
379
+ * positional type information (all positions can contain any element
380
+ * from the original tuple). Default comparison is numeric ascending.
381
+ *
382
+ * @template T - The tuple type to sort
383
+ * @param tpl - The input tuple
384
+ * @param comparator - Optional comparison function (defaults to numeric comparison)
385
+ * @returns A new tuple with sorted elements
386
+ *
387
+ * @example
388
+ * ```typescript
389
+ * // Default numeric sorting
390
+ * const nums = [3, 1, 4, 1, 5] as const;
391
+ * const sorted = Tpl.toSorted(nums); // readonly [1, 1, 3, 4, 5]
392
+ *
393
+ * // Custom comparator
394
+ * const descending = Tpl.toSorted(nums, (a, b) => b - a);
395
+ * // readonly [5, 4, 3, 1, 1]
396
+ *
397
+ * // String sorting with comparator
398
+ * const strs = ['banana', 'apple', 'cherry'] as const;
399
+ * const alphaSorted = Tpl.toSorted(strs, (a, b) => a.localeCompare(b));
400
+ * // readonly ['apple', 'banana', 'cherry']
401
+ *
402
+ * // Mixed types require explicit comparator
403
+ * const mixed = [3, '2', 1, '4'] as const;
404
+ * const mixedSorted = Tpl.toSorted(mixed, (a, b) => Number(a) - Number(b));
405
+ * // readonly ['1', '2', '3', '4'] but typed as (3 | '2' | 1 | '4')[]
406
+ *
407
+ * // Note: Element types become union of all elements
408
+ * type Original = readonly [1, 2, 3];
409
+ * type Sorted = { readonly [K in keyof Original]: Original[number] };
410
+ * // Sorted = readonly [1 | 2 | 3, 1 | 2 | 3, 1 | 2 | 3]
411
+ * ```
412
+ */
413
+ export const toSorted: ToSortedFnOverload = (<
414
+ const T extends readonly unknown[],
415
+ >(
416
+ tpl: T,
417
+ comparator?: (x: T[number], y: T[number]) => number,
418
+ ): { readonly [K in keyof T]: T[number] } => {
419
+ const cmp = comparator ?? ((x, y) => Number(x) - Number(y));
420
+
421
+ return tpl.toSorted(cmp) as {
422
+ readonly [K in keyof T]: T[number];
423
+ };
424
+ }) as ToSortedFnOverload;
425
+
426
+ type ToSortedFnOverload =
427
+ /**
428
+ * Sort tuple with optional comparator function.
429
+ */
430
+ <const T extends readonly unknown[]>(
431
+ tpl: T,
432
+ comparator?: (x: T[number], y: T[number]) => number,
433
+ ) => { readonly [K in keyof T]: T[number] };
434
+
435
+ /**
436
+ * Sorts a tuple by derived values from its elements.
437
+ *
438
+ * Allows sorting complex objects by extracting a sortable value from each.
439
+ * Like `toSorted`, this preserves tuple length but element types become
440
+ * a union of all possible elements.
441
+ *
442
+ * @template T - The tuple type to sort
443
+ * @template B - The type of values used for comparison
444
+ * @param tpl - The input tuple
445
+ * @param comparatorValueMapper - Function to extract comparison value from each element
446
+ * @param comparator - Optional comparator for the extracted values
447
+ * @returns A new sorted tuple
448
+ *
449
+ * @example
450
+ * ```typescript
451
+ * // Sort objects by numeric property
452
+ * const users = [
453
+ * {name: 'Alice', age: 30},
454
+ * {name: 'Bob', age: 20},
455
+ * {name: 'Charlie', age: 25}
456
+ * ] as const;
457
+ * const byAge = Tpl.toSortedBy(users, (user) => user.age);
458
+ * // [{name: 'Bob', age: 20}, {name: 'Charlie', age: 25}, {name: 'Alice', age: 30}]
459
+ *
460
+ * // Sort by string property with custom comparator
461
+ * const byNameDesc = Tpl.toSortedBy(
462
+ * users,
463
+ * (user) => user.name,
464
+ * (a, b) => b.localeCompare(a)
465
+ * );
466
+ * // Sorted by name in descending order
467
+ *
468
+ * // Sort by computed values
469
+ * const points = [{x: 3, y: 4}, {x: 1, y: 1}, {x: 2, y: 2}] as const;
470
+ * const byDistance = Tpl.toSortedBy(
471
+ * points,
472
+ * (p) => Math.sqrt(p.x ** 2 + p.y ** 2)
473
+ * );
474
+ * // Sorted by distance from origin
475
+ *
476
+ * // Custom comparator for complex sorting
477
+ * const items = [{priority: 1, name: 'A'}, {priority: 1, name: 'B'}] as const;
478
+ * const sorted = Tpl.toSortedBy(
479
+ * items,
480
+ * (item) => item.priority,
481
+ * (a, b) => b - a // High priority first
482
+ * );
483
+ * ```
484
+ */
485
+ export const toSortedBy: ToSortedByFnOverload = (<
486
+ const T extends readonly unknown[],
487
+ const B,
488
+ >(
489
+ tpl: T,
490
+ comparatorValueMapper: (value: T[number]) => B,
491
+ comparator?: (x: B, y: B) => number,
492
+ ): Readonly<{ [K in keyof T]: T[number] }> =>
493
+ toSorted(tpl, (x, y) =>
494
+ comparator === undefined
495
+ ? (comparatorValueMapper(x) as number) -
496
+ (comparatorValueMapper(y) as number)
497
+ : comparator(comparatorValueMapper(x), comparatorValueMapper(y)),
498
+ )) as ToSortedByFnOverload;
499
+
500
+ type ToSortedByFnOverload = {
501
+ /**
502
+ * Sort by numeric values (default numeric comparison).
503
+ */
504
+ <const T extends readonly unknown[]>(
505
+ tpl: T,
506
+ comparatorValueMapper: (value: T[number]) => number,
507
+ comparator?: (x: number, y: number) => number,
508
+ ): Readonly<{ [K in keyof T]: T[number] }>;
509
+
510
+ /**
511
+ * Sort by any comparable values with required comparator.
512
+ */
513
+ <const T extends readonly unknown[], const B>(
514
+ tpl: T,
515
+ comparatorValueMapper: (value: T[number]) => B,
516
+ comparator: (x: B, y: B) => number,
517
+ ): Readonly<{ [K in keyof T]: T[number] }>;
518
+ };
519
+ }