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,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time type assertion utility for TypeScript type testing.
|
|
3
|
+
*
|
|
4
|
+
* This function performs static type relationship checking at compile-time and has no runtime effect.
|
|
5
|
+
* It is primarily used in test files to verify that TypeScript's type inference and type relationships
|
|
6
|
+
* work as expected. The function will cause TypeScript compilation errors if the specified type
|
|
7
|
+
* relationship does not hold.
|
|
8
|
+
*
|
|
9
|
+
* ## Supported Type Relations
|
|
10
|
+
*
|
|
11
|
+
* ### Equality Relations
|
|
12
|
+
* - **`"="` (strict equality)**: Asserts that types `A` and `B` are exactly the same type.
|
|
13
|
+
* Uses TypeScript's internal type equality checking.
|
|
14
|
+
* - **`"!="` (strict inequality)**: Asserts that types `A` and `B` are not exactly the same type.
|
|
15
|
+
*
|
|
16
|
+
* ### Assignability Relations
|
|
17
|
+
* - **`"~="` (mutual assignability)**: Asserts that `A` extends `B` AND `B` extends `A`.
|
|
18
|
+
* Types are structurally equivalent and mutually assignable.
|
|
19
|
+
* - **`"<="` (subtype relation)**: Asserts that type `A` extends (is assignable to) type `B`.
|
|
20
|
+
* Type `A` is a subtype of `B`.
|
|
21
|
+
* - **`">="` (supertype relation)**: Asserts that type `B` extends (is assignable to) type `A`.
|
|
22
|
+
* Type `A` is a supertype of `B`.
|
|
23
|
+
*
|
|
24
|
+
* ### Negative Assignability Relations
|
|
25
|
+
* - **`"!<="` (not subtype)**: Asserts that type `A` does NOT extend type `B`.
|
|
26
|
+
* - **`"!>="` (not supertype)**: Asserts that type `B` does NOT extend type `A`.
|
|
27
|
+
*
|
|
28
|
+
* ## Type Parameter Constraints
|
|
29
|
+
*
|
|
30
|
+
* @template A - The first type for comparison. Can be any TypeScript type including:
|
|
31
|
+
* - Primitive types (string, number, boolean, etc.)
|
|
32
|
+
* - Object types and interfaces
|
|
33
|
+
* - Union and intersection types
|
|
34
|
+
* - Generic types and type parameters
|
|
35
|
+
* - Literal types and branded types
|
|
36
|
+
* - Function types and return types
|
|
37
|
+
* @template B - The second type for comparison. Same constraints as type `A`.
|
|
38
|
+
*
|
|
39
|
+
* @param _relation - A string literal representing the expected type relationship.
|
|
40
|
+
* TypeScript's type system automatically infers and restricts the available operators
|
|
41
|
+
* based on the actual relationship between types `A` and `B`. If an invalid relationship
|
|
42
|
+
* is specified, TypeScript will show a compilation error.
|
|
43
|
+
*
|
|
44
|
+
* ## Usage Patterns
|
|
45
|
+
*
|
|
46
|
+
* ### Basic Type Testing
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import { expectType } from './expect-type.mjs';
|
|
50
|
+
*
|
|
51
|
+
* // Primitive type equality
|
|
52
|
+
* expectType<string, string>("="); // ✓ exact match
|
|
53
|
+
* expectType<number, string>("!="); // ✓ different types
|
|
54
|
+
* expectType<42, number>("<="); // ✓ literal extends primitive
|
|
55
|
+
* expectType<number, 42>(">="); // ✓ primitive is supertype
|
|
56
|
+
*
|
|
57
|
+
* // Type assertions will cause compilation errors for wrong relationships:
|
|
58
|
+
* // expectType<string, number>("="); // ❌ TypeScript error
|
|
59
|
+
* // expectType<number, string>("<="); // ❌ TypeScript error
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* ### Array and Tuple Type Validation
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // Testing array utility function return types
|
|
66
|
+
* const zeros = Arr.zeros(3);
|
|
67
|
+
* expectType<typeof zeros, readonly [0, 0, 0]>("=");
|
|
68
|
+
*
|
|
69
|
+
* const sequence = Arr.seq(5);
|
|
70
|
+
* expectType<typeof sequence, readonly [0, 1, 2, 3, 4]>("=");
|
|
71
|
+
*
|
|
72
|
+
* // Dynamic length arrays
|
|
73
|
+
* const dynamicArray = Arr.zeros(someLength);
|
|
74
|
+
* expectType<typeof dynamicArray, readonly 0[]>("=");
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* ### Function Return Type Testing
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* // Testing function return types
|
|
81
|
+
* const createUser = () => ({ id: 1, name: 'John' });
|
|
82
|
+
* expectType<ReturnType<typeof createUser>, { id: number; name: string }>("~=");
|
|
83
|
+
*
|
|
84
|
+
* // Generic function type inference
|
|
85
|
+
* const identity = <T>(x: T): T => x;
|
|
86
|
+
* const result = identity('hello');
|
|
87
|
+
* expectType<typeof result, string>("=");
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* ### Union and Intersection Types
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* // Union type relationships
|
|
94
|
+
* expectType<string, string | number>("<="); // string extends union
|
|
95
|
+
* expectType<string | number, string>(">="); // union contains string
|
|
96
|
+
* expectType<string | number, number>(">="); // union contains number
|
|
97
|
+
*
|
|
98
|
+
* // Intersection type relationships
|
|
99
|
+
* type A = { a: number };
|
|
100
|
+
* type B = { b: string };
|
|
101
|
+
* expectType<A & B, A>(">="); // intersection extends component
|
|
102
|
+
* expectType<A, A & B>("<="); // component extends intersection
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* ### Branded Type Validation
|
|
106
|
+
* @example
|
|
107
|
+
* ```typescript
|
|
108
|
+
* // Testing branded number types
|
|
109
|
+
* expectType<PositiveInt, number>("<="); // branded type extends base
|
|
110
|
+
* expectType<number, PositiveInt>(">="); // base type is supertype
|
|
111
|
+
* expectType<PositiveInt, NegativeInt>("!="); // different branded types
|
|
112
|
+
*
|
|
113
|
+
* // Type guard function validation
|
|
114
|
+
* if (isPositiveInt(value)) {
|
|
115
|
+
* expectType<typeof value, PositiveInt>("<=");
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*
|
|
119
|
+
* ### Optional and Result Type Testing
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* // Optional type narrowing
|
|
123
|
+
* const optional: Optional<number> = Optional.some(42);
|
|
124
|
+
* if (Optional.isSome(optional)) {
|
|
125
|
+
* expectType<typeof optional, Optional.Some<number>>("<=");
|
|
126
|
+
* }
|
|
127
|
+
* if (Optional.isNone(optional)) {
|
|
128
|
+
* expectType<typeof optional, Optional.None>("<=");
|
|
129
|
+
* }
|
|
130
|
+
*
|
|
131
|
+
* // Result type validation
|
|
132
|
+
* const result: Result<string, Error> = Result.ok('success');
|
|
133
|
+
* expectType<typeof result, Result<string, Error>>("<=");
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* ### Type Guard and Validation Testing
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* // Testing type guard functions
|
|
140
|
+
* if (isRecord(value)) {
|
|
141
|
+
* expectType<typeof value, UnknownRecord>("<=");
|
|
142
|
+
* }
|
|
143
|
+
*
|
|
144
|
+
* // Testing compile-time type predicates
|
|
145
|
+
* const obj = { key: 'value' };
|
|
146
|
+
* if (hasKey(obj, 'key')) {
|
|
147
|
+
* expectType<typeof obj.key, unknown>("<=");
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* ## Common Testing Patterns
|
|
152
|
+
*
|
|
153
|
+
* ### Dual Testing Strategy
|
|
154
|
+
* Combine `expectType` with runtime assertions for comprehensive testing:
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```typescript
|
|
158
|
+
* describe('Arr.zeros', () => {
|
|
159
|
+
* test('should create array of zeros with correct type', () => {
|
|
160
|
+
* const result = Arr.zeros(3);
|
|
161
|
+
*
|
|
162
|
+
* // Compile-time type assertion
|
|
163
|
+
* expectType<typeof result, readonly [0, 0, 0]>("=");
|
|
164
|
+
*
|
|
165
|
+
* // Runtime behavior assertion
|
|
166
|
+
* expect(result).toStrictEqual([0, 0, 0]);
|
|
167
|
+
* });
|
|
168
|
+
* });
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* ### Type Relationship Validation
|
|
172
|
+
* Test complex type hierarchies and relationships:
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* // Ensure proper type hierarchy
|
|
177
|
+
* expectType<PositiveInt, Int>("<="); // positive is subset of int
|
|
178
|
+
* expectType<Int, FiniteNumber>("<="); // int is subset of finite
|
|
179
|
+
* expectType<FiniteNumber, number>("<="); // finite is subset of number
|
|
180
|
+
*
|
|
181
|
+
* // Verify mutual exclusion
|
|
182
|
+
* expectType<PositiveInt, NegativeInt>("!="); // different int types
|
|
183
|
+
* expectType<PositiveInt, NegativeInt>("!<="); // neither extends the other
|
|
184
|
+
* expectType<NegativeInt, PositiveInt>("!<=");
|
|
185
|
+
* ```
|
|
186
|
+
*
|
|
187
|
+
* ## Important Notes
|
|
188
|
+
*
|
|
189
|
+
* - **Compile-time only**: This function has no runtime behavior and will be optimized away.
|
|
190
|
+
* - **Type inference**: The available relation operators are automatically inferred by TypeScript
|
|
191
|
+
* based on the actual type relationship between `A` and `B`.
|
|
192
|
+
* - **Error feedback**: Invalid type relationships will cause clear TypeScript compilation errors.
|
|
193
|
+
* - **Test organization**: Typically used in `.test.mts` files alongside runtime assertions.
|
|
194
|
+
* - **Performance**: Has zero runtime overhead as it's purely a compile-time construct.
|
|
195
|
+
*
|
|
196
|
+
* @since 1.0.0
|
|
197
|
+
*/
|
|
198
|
+
export const expectType = <A, B>(
|
|
199
|
+
_relation: TypeEq<A, B> extends true
|
|
200
|
+
? '<=' | '=' | '>=' | '~='
|
|
201
|
+
:
|
|
202
|
+
| '!='
|
|
203
|
+
| (TypeExtends<A, B> extends true
|
|
204
|
+
? '<=' | (TypeExtends<B, A> extends true ? '>=' | '~=' : '!>=')
|
|
205
|
+
: '!<=' | (TypeExtends<B, A> extends true ? '>=' : '!>=')),
|
|
206
|
+
): void => undefined;
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { keyIsIn } from '../guard/index.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @internal
|
|
6
|
+
* Utility type to extract the union of all values from a record type.
|
|
7
|
+
* @template T The record type to extract values from.
|
|
8
|
+
*/
|
|
9
|
+
type ValueOf<T> = T[keyof T];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @internal
|
|
13
|
+
* Represents a record with unknown value types.
|
|
14
|
+
*/
|
|
15
|
+
type UnknownRecord = Record<PropertyKey, unknown>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @internal
|
|
19
|
+
* Represents a readonly record mapping keys of type K to values of type V.
|
|
20
|
+
* @template K The type of keys.
|
|
21
|
+
* @template V The type of values.
|
|
22
|
+
*/
|
|
23
|
+
type ReadonlyRecord<K extends PropertyKey, V> = Readonly<Record<K, V>>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @internal
|
|
27
|
+
* A relaxed version of Exclude that handles edge cases with property keys.
|
|
28
|
+
* @template T The type to exclude from.
|
|
29
|
+
* @template U The type to exclude.
|
|
30
|
+
*/
|
|
31
|
+
type RelaxedExclude<T, U> = T extends U ? never : T;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @internal
|
|
35
|
+
* Checks if two types are exactly equal.
|
|
36
|
+
* @template T First type to compare.
|
|
37
|
+
* @template U Second type to compare.
|
|
38
|
+
*/
|
|
39
|
+
type TypeEq<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Type-safe pattern matching function for string-based discriminated unions.
|
|
43
|
+
*
|
|
44
|
+
* Provides compile-time guarantees for exhaustive case handling when working with
|
|
45
|
+
* literal string unions. Automatically enforces completeness checking when all
|
|
46
|
+
* cases are covered, and requires a default value when cases are incomplete.
|
|
47
|
+
*
|
|
48
|
+
* ## Key Features:
|
|
49
|
+
* - **Exhaustive Matching**: When all cases of a literal union are handled, no default value is needed
|
|
50
|
+
* - **Partial Matching**: When cases are incomplete or working with general string types, a default value is required
|
|
51
|
+
* - **Type Safety**: Prevents extra cases and ensures only valid keys are used
|
|
52
|
+
* - **Strict Property Checking**: Rejects objects with unexpected properties
|
|
53
|
+
*
|
|
54
|
+
* @param target - The value to match against
|
|
55
|
+
* @param cases - Object mapping possible values to their corresponding results
|
|
56
|
+
* @param defaultValue - Fallback value (required when not all cases are covered)
|
|
57
|
+
* @returns The matched result or default value
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* Exhaustive matching (no default needed):
|
|
61
|
+
* ```typescript
|
|
62
|
+
* type Status = 'loading' | 'success' | 'error';
|
|
63
|
+
* const status: Status = 'loading';
|
|
64
|
+
*
|
|
65
|
+
* const message = match(status, {
|
|
66
|
+
* loading: 'Please wait...',
|
|
67
|
+
* success: 'Operation completed!',
|
|
68
|
+
* error: 'Something went wrong'
|
|
69
|
+
* });
|
|
70
|
+
* // Type: string
|
|
71
|
+
* // Result: 'Please wait...'
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* Partial matching (default required):
|
|
76
|
+
* ```typescript
|
|
77
|
+
* type Priority = 'low' | 'medium' | 'high' | 'critical';
|
|
78
|
+
* const priority: Priority = 'medium';
|
|
79
|
+
*
|
|
80
|
+
* const color = match(priority, {
|
|
81
|
+
* high: 'red',
|
|
82
|
+
* critical: 'darkred'
|
|
83
|
+
* }, 'gray'); // Default required for uncovered cases
|
|
84
|
+
* // Type: 'red' | 'darkred' | 'gray'
|
|
85
|
+
* // Result: 'gray'
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* Working with general string types:
|
|
90
|
+
* ```typescript
|
|
91
|
+
* const userInput: string = getUserInput();
|
|
92
|
+
*
|
|
93
|
+
* const route = match(userInput, {
|
|
94
|
+
* 'home': '/',
|
|
95
|
+
* 'about': '/about',
|
|
96
|
+
* 'contact': '/contact'
|
|
97
|
+
* }, '/404'); // Default required for string type
|
|
98
|
+
* // Type: '/' | '/about' | '/contact' | '/404'
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* HTTP status code handling:
|
|
103
|
+
* ```typescript
|
|
104
|
+
* type HttpStatus = 200 | 404 | 500;
|
|
105
|
+
* const status: HttpStatus = 404;
|
|
106
|
+
*
|
|
107
|
+
* const response = match(String(status), {
|
|
108
|
+
* '200': { ok: true, message: 'Success' },
|
|
109
|
+
* '404': { ok: false, message: 'Not Found' },
|
|
110
|
+
* '500': { ok: false, message: 'Server Error' }
|
|
111
|
+
* });
|
|
112
|
+
* // All cases covered, no default needed
|
|
113
|
+
* // Result: { ok: false, message: 'Not Found' }
|
|
114
|
+
* ```
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* Complex discriminated union handling:
|
|
118
|
+
* ```typescript
|
|
119
|
+
* type ApiResponse =
|
|
120
|
+
* | { status: 'loading' }
|
|
121
|
+
* | { status: 'success'; data: string }
|
|
122
|
+
* | { status: 'error'; error: string };
|
|
123
|
+
*
|
|
124
|
+
* const handleResponse = (response: ApiResponse) =>
|
|
125
|
+
* match(response.status, {
|
|
126
|
+
* loading: 'Please wait...',
|
|
127
|
+
* success: 'Data loaded successfully!',
|
|
128
|
+
* error: 'Failed to load data'
|
|
129
|
+
* });
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* Advanced usage with functional composition:
|
|
134
|
+
* ```typescript
|
|
135
|
+
* // Creating reusable matchers
|
|
136
|
+
* const logLevelToColor = (level: string) => match(level, {
|
|
137
|
+
* 'debug': 'gray',
|
|
138
|
+
* 'info': 'blue',
|
|
139
|
+
* 'warn': 'yellow',
|
|
140
|
+
* 'error': 'red'
|
|
141
|
+
* }, 'black'); // Default for unknown levels
|
|
142
|
+
*
|
|
143
|
+
* const logLevelToIcon = (level: string) => match(level, {
|
|
144
|
+
* 'debug': '🐛',
|
|
145
|
+
* 'info': 'ℹ️',
|
|
146
|
+
* 'warn': '⚠️',
|
|
147
|
+
* 'error': '❌'
|
|
148
|
+
* }, '📝');
|
|
149
|
+
*
|
|
150
|
+
* // Combining matchers
|
|
151
|
+
* const formatLogEntry = (level: string, message: string) => ({
|
|
152
|
+
* color: logLevelToColor(level),
|
|
153
|
+
* icon: logLevelToIcon(level),
|
|
154
|
+
* text: `${logLevelToIcon(level)} ${message}`
|
|
155
|
+
* });
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export const match: MatchFnOverload = (<
|
|
159
|
+
const Case extends string,
|
|
160
|
+
const R extends UnknownRecord,
|
|
161
|
+
const D,
|
|
162
|
+
>(
|
|
163
|
+
...args:
|
|
164
|
+
| readonly [target: Case, cases: R]
|
|
165
|
+
| readonly [target: Case, cases: R, defaultValue: D]
|
|
166
|
+
): ValueOf<R> | D => {
|
|
167
|
+
switch (args.length) {
|
|
168
|
+
case 2: {
|
|
169
|
+
const [target, cases] = args;
|
|
170
|
+
return cases[target];
|
|
171
|
+
}
|
|
172
|
+
case 3: {
|
|
173
|
+
const [target, cases, defaultValue] = args;
|
|
174
|
+
if (keyIsIn(target, cases)) {
|
|
175
|
+
return cases[target];
|
|
176
|
+
} else {
|
|
177
|
+
return defaultValue;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}) as MatchFnOverload;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @internal
|
|
185
|
+
* Overloaded function type for the match function.
|
|
186
|
+
* Provides different signatures based on whether exhaustive matching is possible.
|
|
187
|
+
* @template Case The string literal union type to match against.
|
|
188
|
+
* @template R The record type containing the case mappings.
|
|
189
|
+
* @template D The type of the default value.
|
|
190
|
+
*/
|
|
191
|
+
type MatchFnOverload = {
|
|
192
|
+
/**
|
|
193
|
+
* Exhaustive matching signature - used when all cases in a literal union are covered.
|
|
194
|
+
* No default value is required or allowed.
|
|
195
|
+
* @template Case The string literal union type.
|
|
196
|
+
* @template R The record type with mappings for all cases.
|
|
197
|
+
* @param target The value to match.
|
|
198
|
+
* @param cases Object mapping all possible case values to results.
|
|
199
|
+
* @returns The matched result value.
|
|
200
|
+
*/
|
|
201
|
+
<const Case extends string, const R extends ReadonlyRecord<Case, unknown>>(
|
|
202
|
+
target: Case,
|
|
203
|
+
cases: StrictPropertyCheck<R, Case>,
|
|
204
|
+
): R[Case];
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Partial matching signature - used when not all cases are covered or when dealing with general string types.
|
|
208
|
+
* A default value is required.
|
|
209
|
+
* @template Case The string type (may be literal union or general string).
|
|
210
|
+
* @template R The record type with partial case mappings.
|
|
211
|
+
* @template D The type of the default value.
|
|
212
|
+
* @param target The value to match.
|
|
213
|
+
* @param cases Object mapping some case values to results.
|
|
214
|
+
* @param defaultValue Required fallback value for unmatched cases.
|
|
215
|
+
* @returns The matched result value or the default value.
|
|
216
|
+
*/
|
|
217
|
+
<const Case extends string, const R extends UnknownRecord, const D>(
|
|
218
|
+
target: Case,
|
|
219
|
+
cases: StrictPropertyCheck<R, Case>,
|
|
220
|
+
defaultValue: IsLiteralUnionFullyCovered<Case, R> extends true ? never : D,
|
|
221
|
+
): ValueOf<R> | D;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @internal
|
|
226
|
+
* Helper type to ensure that an object `T` only contains keys specified in `ExpectedKeys`.
|
|
227
|
+
* If `T` has any keys not in `ExpectedKeys`, this type resolves to `never`.
|
|
228
|
+
* @template T The object type to check.
|
|
229
|
+
* @template ExpectedKeys The union of string literal types representing the allowed keys.
|
|
230
|
+
*/
|
|
231
|
+
type StrictPropertyCheck<T, ExpectedKeys extends PropertyKey> =
|
|
232
|
+
RelaxedExclude<keyof T, ExpectedKeys> extends never ? T : never;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @internal
|
|
236
|
+
* Helper type to check if all cases in `Case` union are fully covered by keys in `R`.
|
|
237
|
+
* This checks bidirectional coverage: all Case members are in R, and no extra keys.
|
|
238
|
+
* @template Case A union of string literal types representing the possible cases.
|
|
239
|
+
* @template R A record type.
|
|
240
|
+
*/
|
|
241
|
+
type AllCasesCovered<Case extends PropertyKey, R> =
|
|
242
|
+
TypeEq<Case, keyof R> extends true ? true : false;
|
|
243
|
+
|
|
244
|
+
expectType<AllCasesCovered<'a' | 'b', { a: 1; b: 2 }>, true>('=');
|
|
245
|
+
expectType<AllCasesCovered<'a' | 'b' | 'c', { a: 1; b: 2 }>, false>('=');
|
|
246
|
+
expectType<AllCasesCovered<'a' | 'b', { a: 1; b: 2; c: 3 }>, false>('=');
|
|
247
|
+
expectType<AllCasesCovered<string, Record<string, string>>, true>('=');
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @internal
|
|
251
|
+
* Helper type to check if Case is a literal union type and all cases are covered.
|
|
252
|
+
* @template Case A union of string literal types.
|
|
253
|
+
* @template R A record type.
|
|
254
|
+
*/
|
|
255
|
+
type IsLiteralUnionFullyCovered<
|
|
256
|
+
Case extends PropertyKey,
|
|
257
|
+
R extends UnknownRecord,
|
|
258
|
+
> =
|
|
259
|
+
TypeEq<IsLiteralType<Case>, true> extends true
|
|
260
|
+
? AllCasesCovered<Case, R>
|
|
261
|
+
: false;
|
|
262
|
+
|
|
263
|
+
expectType<IsLiteralUnionFullyCovered<'a' | 'b', { a: 1; b: 2 }>, true>('=');
|
|
264
|
+
expectType<IsLiteralUnionFullyCovered<'a' | 'b' | 'c', { a: 1; b: 2 }>, false>(
|
|
265
|
+
'=',
|
|
266
|
+
);
|
|
267
|
+
expectType<IsLiteralUnionFullyCovered<'a' | 'b', { a: 1; b: 2; c: 3 }>, false>(
|
|
268
|
+
'=',
|
|
269
|
+
);
|
|
270
|
+
expectType<IsLiteralUnionFullyCovered<string, Record<string, string>>, false>(
|
|
271
|
+
'=',
|
|
272
|
+
);
|
|
273
|
+
expectType<
|
|
274
|
+
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
275
|
+
IsLiteralUnionFullyCovered<'a' | 'b' | string, { a: 1; b: 2 }>,
|
|
276
|
+
false
|
|
277
|
+
>('=');
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @internal
|
|
281
|
+
* Helper type to determine if a given PropertyKey `T` is a literal type (e.g., 'a', 1)
|
|
282
|
+
* or a general type (e.g., string, number).
|
|
283
|
+
* @template T The PropertyKey type to check.
|
|
284
|
+
* @returns `true` if `T` is a literal type, `false` otherwise.
|
|
285
|
+
*/
|
|
286
|
+
type IsLiteralType<T extends PropertyKey> = string extends T
|
|
287
|
+
? false
|
|
288
|
+
: number extends T
|
|
289
|
+
? false
|
|
290
|
+
: symbol extends T
|
|
291
|
+
? false
|
|
292
|
+
: true;
|
|
293
|
+
|
|
294
|
+
expectType<IsLiteralType<'a' | 'b'>, true>('=');
|
|
295
|
+
expectType<IsLiteralType<'a'>, true>('=');
|
|
296
|
+
expectType<IsLiteralType<string>, false>('=');
|
|
297
|
+
expectType<IsLiteralType<number>, false>('=');
|
|
298
|
+
expectType<IsLiteralType<1>, true>('=');
|
|
299
|
+
expectType<IsLiteralType<number | 'aa'>, false>('=');
|
|
300
|
+
expectType<IsLiteralType<'aa' | 32>, true>('=');
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { match } from './match.mjs';
|
|
3
|
+
|
|
4
|
+
describe('match', () => {
|
|
5
|
+
type Direction = 'E' | 'N' | 'S' | 'W';
|
|
6
|
+
const direction: Direction = 'N' as Direction;
|
|
7
|
+
|
|
8
|
+
test('literal union', () => {
|
|
9
|
+
const res = match(direction, {
|
|
10
|
+
E: 2,
|
|
11
|
+
N: 3,
|
|
12
|
+
S: 4,
|
|
13
|
+
W: 5,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
expectType<typeof res, 2 | 3 | 4 | 5>('=');
|
|
17
|
+
|
|
18
|
+
expect(res).toBe(3);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('Literal union with missing key - requires default', () => {
|
|
22
|
+
const res = match(
|
|
23
|
+
direction,
|
|
24
|
+
{
|
|
25
|
+
E: 2,
|
|
26
|
+
S: 4,
|
|
27
|
+
W: 5,
|
|
28
|
+
},
|
|
29
|
+
999,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expectType<typeof res, 2 | 4 | 5 | 999>('=');
|
|
33
|
+
|
|
34
|
+
expect(res).toBe(999);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('Literal union with missing key - 2-arg form should cause type error', () => {
|
|
38
|
+
// @ts-expect-error Cannot use 2-argument form when not all cases are covered
|
|
39
|
+
const res = match(direction, {
|
|
40
|
+
E: 2,
|
|
41
|
+
S: 4,
|
|
42
|
+
W: 5,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expectType<typeof res, unknown>('=');
|
|
46
|
+
|
|
47
|
+
expect(res).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('Missing key cases with string - requires default', () => {
|
|
51
|
+
const res = match(
|
|
52
|
+
direction as string,
|
|
53
|
+
{
|
|
54
|
+
E: 2,
|
|
55
|
+
S: 4,
|
|
56
|
+
W: 5,
|
|
57
|
+
},
|
|
58
|
+
'default',
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expectType<typeof res, 2 | 4 | 5 | 'default'>('=');
|
|
62
|
+
|
|
63
|
+
expect(res).toBe('default');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('String type always requires default even with all keys', () => {
|
|
67
|
+
const res = match(
|
|
68
|
+
direction as string,
|
|
69
|
+
{
|
|
70
|
+
E: 2,
|
|
71
|
+
N: 3,
|
|
72
|
+
S: 4,
|
|
73
|
+
W: 5,
|
|
74
|
+
},
|
|
75
|
+
'default',
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expectType<typeof res, 2 | 3 | 4 | 5 | 'default'>('=');
|
|
79
|
+
|
|
80
|
+
expect(res).toBe(3);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('A case with excess properties', () => {
|
|
84
|
+
// @ts-expect-error excess properties
|
|
85
|
+
const res = match(direction, {
|
|
86
|
+
E: 2,
|
|
87
|
+
N: 3,
|
|
88
|
+
S: 4,
|
|
89
|
+
W: 5,
|
|
90
|
+
X: 0,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expectType<typeof res, 2 | 3 | 4 | 5>('=');
|
|
94
|
+
|
|
95
|
+
expect(res).toBe(3);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('with default case - all keys present should cause type error', () => {
|
|
99
|
+
const res = match(
|
|
100
|
+
direction,
|
|
101
|
+
{
|
|
102
|
+
E: 2,
|
|
103
|
+
N: 3,
|
|
104
|
+
S: 4,
|
|
105
|
+
W: 5,
|
|
106
|
+
},
|
|
107
|
+
// @ts-expect-error When all cases are covered, default value should not be allowed
|
|
108
|
+
999,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(res).toBe(3);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('all keys present without default - should work correctly', () => {
|
|
115
|
+
const res = match(direction, {
|
|
116
|
+
E: 2,
|
|
117
|
+
N: 3,
|
|
118
|
+
S: 4,
|
|
119
|
+
W: 5,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expectType<typeof res, 2 | 3 | 4 | 5>('=');
|
|
123
|
+
|
|
124
|
+
expect(res).toBe(3);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('with default case - missing key', () => {
|
|
128
|
+
const res = match(
|
|
129
|
+
direction,
|
|
130
|
+
{
|
|
131
|
+
E: 2,
|
|
132
|
+
S: 4,
|
|
133
|
+
W: 5,
|
|
134
|
+
},
|
|
135
|
+
999,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expectType<typeof res, 2 | 4 | 5 | 999>('=');
|
|
139
|
+
|
|
140
|
+
expect(res).toBe(999);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('with default case - string key', () => {
|
|
144
|
+
const res = match(
|
|
145
|
+
direction as string,
|
|
146
|
+
{
|
|
147
|
+
E: 2,
|
|
148
|
+
N: 3,
|
|
149
|
+
S: 4,
|
|
150
|
+
W: 5,
|
|
151
|
+
},
|
|
152
|
+
'default',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expectType<typeof res, 2 | 3 | 4 | 5 | 'default'>('=');
|
|
156
|
+
|
|
157
|
+
expect(res).toBe(3);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('with default case - string key missing', () => {
|
|
161
|
+
const unknownDirection = 'X' as string;
|
|
162
|
+
const res = match(
|
|
163
|
+
unknownDirection,
|
|
164
|
+
{
|
|
165
|
+
E: 2,
|
|
166
|
+
N: 3,
|
|
167
|
+
S: 4,
|
|
168
|
+
W: 5,
|
|
169
|
+
},
|
|
170
|
+
'default',
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expectType<typeof res, 2 | 3 | 4 | 5 | 'default'>('=');
|
|
174
|
+
|
|
175
|
+
expect(res).toBe('default');
|
|
176
|
+
});
|
|
177
|
+
});
|