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.
- package/LICENSE +201 -0
- package/README.md +534 -0
- package/package.json +101 -0
- package/src/array/array-utils-creation.test.mts +443 -0
- package/src/array/array-utils-modification.test.mts +197 -0
- package/src/array/array-utils-overload-type-error.test.mts +149 -0
- package/src/array/array-utils-reducing-value.test.mts +425 -0
- package/src/array/array-utils-search.test.mts +169 -0
- package/src/array/array-utils-set-op.test.mts +335 -0
- package/src/array/array-utils-slice-clamped.test.mts +113 -0
- package/src/array/array-utils-slicing.test.mts +316 -0
- package/src/array/array-utils-transformation.test.mts +790 -0
- package/src/array/array-utils-validation.test.mts +492 -0
- package/src/array/array-utils.mts +4000 -0
- package/src/array/array.test.mts +146 -0
- package/src/array/index.mts +2 -0
- package/src/array/tuple-utils.mts +519 -0
- package/src/array/tuple-utils.test.mts +518 -0
- package/src/collections/imap-mapped.mts +801 -0
- package/src/collections/imap-mapped.test.mts +860 -0
- package/src/collections/imap.mts +651 -0
- package/src/collections/imap.test.mts +932 -0
- package/src/collections/index.mts +6 -0
- package/src/collections/iset-mapped.mts +889 -0
- package/src/collections/iset-mapped.test.mts +1187 -0
- package/src/collections/iset.mts +682 -0
- package/src/collections/iset.test.mts +1084 -0
- package/src/collections/queue.mts +390 -0
- package/src/collections/queue.test.mts +282 -0
- package/src/collections/stack.mts +423 -0
- package/src/collections/stack.test.mts +225 -0
- package/src/expect-type.mts +206 -0
- package/src/functional/index.mts +4 -0
- package/src/functional/match.mts +300 -0
- package/src/functional/match.test.mts +177 -0
- package/src/functional/optional.mts +733 -0
- package/src/functional/optional.test.mts +619 -0
- package/src/functional/pipe.mts +212 -0
- package/src/functional/pipe.test.mts +85 -0
- package/src/functional/result.mts +1134 -0
- package/src/functional/result.test.mts +777 -0
- package/src/globals.d.mts +38 -0
- package/src/guard/has-key.mts +119 -0
- package/src/guard/has-key.test.mts +219 -0
- package/src/guard/index.mts +7 -0
- package/src/guard/is-non-empty-string.mts +108 -0
- package/src/guard/is-non-empty-string.test.mts +91 -0
- package/src/guard/is-non-null-object.mts +106 -0
- package/src/guard/is-non-null-object.test.mts +90 -0
- package/src/guard/is-primitive.mts +165 -0
- package/src/guard/is-primitive.test.mts +102 -0
- package/src/guard/is-record.mts +153 -0
- package/src/guard/is-record.test.mts +112 -0
- package/src/guard/is-type.mts +450 -0
- package/src/guard/is-type.test.mts +496 -0
- package/src/guard/key-is-in.mts +163 -0
- package/src/guard/key-is-in.test.mts +19 -0
- package/src/index.mts +10 -0
- package/src/iterator/index.mts +1 -0
- package/src/iterator/range.mts +120 -0
- package/src/iterator/range.test.mts +33 -0
- package/src/json/index.mts +1 -0
- package/src/json/json.mts +711 -0
- package/src/json/json.test.mts +628 -0
- package/src/number/branded-types/finite-number.mts +354 -0
- package/src/number/branded-types/finite-number.test.mts +135 -0
- package/src/number/branded-types/index.mts +26 -0
- package/src/number/branded-types/int.mts +278 -0
- package/src/number/branded-types/int.test.mts +140 -0
- package/src/number/branded-types/int16.mts +192 -0
- package/src/number/branded-types/int16.test.mts +170 -0
- package/src/number/branded-types/int32.mts +193 -0
- package/src/number/branded-types/int32.test.mts +170 -0
- package/src/number/branded-types/non-negative-finite-number.mts +223 -0
- package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
- package/src/number/branded-types/non-negative-int16.mts +187 -0
- package/src/number/branded-types/non-negative-int16.test.mts +201 -0
- package/src/number/branded-types/non-negative-int32.mts +187 -0
- package/src/number/branded-types/non-negative-int32.test.mts +204 -0
- package/src/number/branded-types/non-zero-finite-number.mts +229 -0
- package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
- package/src/number/branded-types/non-zero-int.mts +167 -0
- package/src/number/branded-types/non-zero-int.test.mts +177 -0
- package/src/number/branded-types/non-zero-int16.mts +196 -0
- package/src/number/branded-types/non-zero-int16.test.mts +195 -0
- package/src/number/branded-types/non-zero-int32.mts +196 -0
- package/src/number/branded-types/non-zero-int32.test.mts +197 -0
- package/src/number/branded-types/non-zero-safe-int.mts +196 -0
- package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
- package/src/number/branded-types/non-zero-uint16.mts +189 -0
- package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
- package/src/number/branded-types/non-zero-uint32.mts +189 -0
- package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
- package/src/number/branded-types/positive-finite-number.mts +241 -0
- package/src/number/branded-types/positive-finite-number.test.mts +204 -0
- package/src/number/branded-types/positive-int.mts +304 -0
- package/src/number/branded-types/positive-int.test.mts +176 -0
- package/src/number/branded-types/positive-int16.mts +188 -0
- package/src/number/branded-types/positive-int16.test.mts +197 -0
- package/src/number/branded-types/positive-int32.mts +188 -0
- package/src/number/branded-types/positive-int32.test.mts +197 -0
- package/src/number/branded-types/positive-safe-int.mts +187 -0
- package/src/number/branded-types/positive-safe-int.test.mts +210 -0
- package/src/number/branded-types/positive-uint16.mts +188 -0
- package/src/number/branded-types/positive-uint16.test.mts +203 -0
- package/src/number/branded-types/positive-uint32.mts +188 -0
- package/src/number/branded-types/positive-uint32.test.mts +203 -0
- package/src/number/branded-types/safe-int.mts +291 -0
- package/src/number/branded-types/safe-int.test.mts +170 -0
- package/src/number/branded-types/safe-uint.mts +187 -0
- package/src/number/branded-types/safe-uint.test.mts +176 -0
- package/src/number/branded-types/uint.mts +179 -0
- package/src/number/branded-types/uint.test.mts +158 -0
- package/src/number/branded-types/uint16.mts +186 -0
- package/src/number/branded-types/uint16.test.mts +170 -0
- package/src/number/branded-types/uint32.mts +218 -0
- package/src/number/branded-types/uint32.test.mts +170 -0
- package/src/number/enum/index.mts +2 -0
- package/src/number/enum/int8.mts +344 -0
- package/src/number/enum/int8.test.mts +180 -0
- package/src/number/enum/uint8.mts +293 -0
- package/src/number/enum/uint8.test.mts +164 -0
- package/src/number/index.mts +4 -0
- package/src/number/num.mts +604 -0
- package/src/number/num.test.mts +242 -0
- package/src/number/refined-number-utils.mts +566 -0
- package/src/object/index.mts +1 -0
- package/src/object/object.mts +447 -0
- package/src/object/object.test.mts +124 -0
- package/src/others/cast-mutable.mts +113 -0
- package/src/others/cast-readonly.mts +192 -0
- package/src/others/cast-readonly.test.mts +89 -0
- package/src/others/if-then.mts +98 -0
- package/src/others/if-then.test.mts +75 -0
- package/src/others/index.mts +7 -0
- package/src/others/map-nullable.mts +172 -0
- package/src/others/map-nullable.test.mts +297 -0
- package/src/others/memoize-function.mts +196 -0
- package/src/others/memoize-function.test.mts +168 -0
- package/src/others/tuple.mts +160 -0
- package/src/others/tuple.test.mts +11 -0
- package/src/others/unknown-to-string.mts +215 -0
- 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,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
|
+
}
|