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,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,4 @@
1
+ export * from './match.mjs';
2
+ export * from './optional.mjs';
3
+ export * from './pipe.mjs';
4
+ export * from './result.mjs';
@@ -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
+ });