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,192 @@
1
+ /**
2
+ * Casts a mutable type `T` to its `Readonly<T>` equivalent.
3
+ *
4
+ * This is a safe type assertion that adds immutability constraints at the type level.
5
+ * The runtime value remains unchanged - only TypeScript's view of it becomes readonly.
6
+ * This helps prevent accidental mutations and makes code intentions clearer.
7
+ *
8
+ * @template T - The type of the mutable value
9
+ * @param mutable - The mutable value to cast to readonly
10
+ * @returns The same value with readonly modifiers added to its type
11
+ *
12
+ * @example Basic usage with arrays and objects
13
+ * ```typescript
14
+ * const mutableArr: number[] = [1, 2, 3];
15
+ * const readonlyArr = castReadonly(mutableArr);
16
+ * // readonlyArr.push(4); // ❌ TypeScript Error: no 'push' on readonly array
17
+ *
18
+ * const mutableObj = { x: 1, y: 2 };
19
+ * const readonlyObj = castReadonly(mutableObj);
20
+ * // readonlyObj.x = 5; // ❌ TypeScript Error: cannot assign to readonly property
21
+ * ```
22
+ *
23
+ * @example Protecting function return values
24
+ * ```typescript
25
+ * // Prevent callers from mutating internal state
26
+ * class UserService {
27
+ * private users: User[] = [];
28
+ *
29
+ * getUsers(): readonly User[] {
30
+ * return castReadonly(this.users); // Callers can't mutate the array
31
+ * }
32
+ * }
33
+ *
34
+ * const service = new UserService();
35
+ * const users = service.getUsers();
36
+ * // users.push(newUser); // ❌ TypeScript prevents this
37
+ * ```
38
+ *
39
+ * @example Creating immutable configurations
40
+ * ```typescript
41
+ * // Start with mutable object for initialization
42
+ * const config = {
43
+ * apiUrl: 'https://api.example.com',
44
+ * timeout: 5000,
45
+ * retries: 3
46
+ * };
47
+ *
48
+ * // Validate and process config...
49
+ *
50
+ * // Export as readonly to prevent modifications
51
+ * export const APP_CONFIG = castReadonly(config);
52
+ * // APP_CONFIG.timeout = 10000; // ❌ TypeScript Error
53
+ * ```
54
+ *
55
+ * @example Working with array methods
56
+ * ```typescript
57
+ * const numbers: number[] = [1, 2, 3, 4, 5];
58
+ * const readonlyNumbers = castReadonly(numbers);
59
+ *
60
+ * // Read operations still work
61
+ * const doubled = readonlyNumbers.map(n => n * 2); // ✅ Returns new array
62
+ * const sum = readonlyNumbers.reduce((a, b) => a + b, 0); // ✅ Works
63
+ * const first = readonlyNumbers[0]; // ✅ Reading is allowed
64
+ *
65
+ * // Mutations are prevented
66
+ * // readonlyNumbers[0] = 10; // ❌ TypeScript Error
67
+ * // readonlyNumbers.sort(); // ❌ TypeScript Error (sort mutates)
68
+ * ```
69
+ *
70
+ * @see castDeepReadonly - For deeply nested structures
71
+ * @see castMutable - For the opposite operation (use with caution)
72
+ */
73
+ export const castReadonly = <T,>(mutable: T): Readonly<T> =>
74
+ mutable as Readonly<T>;
75
+
76
+ /**
77
+ * Casts a mutable type `T` to its `DeepReadonly<T>` equivalent, recursively adding readonly modifiers.
78
+ *
79
+ * This is a safe type assertion that adds immutability constraints at ALL levels of nesting.
80
+ * Provides complete protection against mutations in complex data structures.
81
+ * The runtime value is unchanged - only TypeScript's type checking is enhanced.
82
+ *
83
+ * @template T - The type of the mutable value
84
+ * @param mutable - The mutable value to cast to deeply readonly
85
+ * @returns The same value with readonly modifiers recursively added to all properties
86
+ *
87
+ * @example Basic usage with nested structures
88
+ * ```typescript
89
+ * const mutableNested = {
90
+ * a: { b: [1, 2, 3] },
91
+ * c: { d: { e: 'value' } }
92
+ * };
93
+ *
94
+ * const readonlyNested = castDeepReadonly(mutableNested);
95
+ * // readonlyNested.a.b.push(4); // ❌ Error: readonly at all levels
96
+ * // readonlyNested.c.d.e = 'new'; // ❌ Error: readonly at all levels
97
+ * // readonlyNested.a = {}; // ❌ Error: cannot reassign readonly property
98
+ * ```
99
+ *
100
+ * @example Protecting complex state objects
101
+ * ```typescript
102
+ * interface AppState {
103
+ * user: {
104
+ * id: number;
105
+ * profile: {
106
+ * name: string;
107
+ * settings: {
108
+ * theme: string;
109
+ * notifications: boolean[];
110
+ * };
111
+ * };
112
+ * };
113
+ * data: {
114
+ * items: Array<{ id: number; value: string }>;
115
+ * };
116
+ * }
117
+ *
118
+ * class StateManager {
119
+ * private state: AppState = initialState;
120
+ *
121
+ * getState(): DeepReadonly<AppState> {
122
+ * return castDeepReadonly(this.state);
123
+ * }
124
+ *
125
+ * // Mutations only allowed through specific methods
126
+ * updateTheme(theme: string) {
127
+ * this.state.user.profile.settings.theme = theme;
128
+ * }
129
+ * }
130
+ * ```
131
+ *
132
+ * @example Creating immutable API responses
133
+ * ```typescript
134
+ * async function fetchUserData(): Promise<DeepReadonly<UserData>> {
135
+ * const response = await api.get<UserData>('/user');
136
+ *
137
+ * // Process and validate data...
138
+ *
139
+ * // Return as deeply immutable to prevent accidental mutations
140
+ * return castDeepReadonly(response.data);
141
+ * }
142
+ *
143
+ * const userData = await fetchUserData();
144
+ * // userData is fully protected from mutations at any depth
145
+ * // userData.preferences.emails.push('new@email.com'); // ❌ TypeScript Error
146
+ * ```
147
+ *
148
+ * @example Working with Redux or state management
149
+ * ```typescript
150
+ * // Redux reducer example
151
+ * type State = DeepReadonly<AppState>;
152
+ *
153
+ * function reducer(state: State, action: Action): State {
154
+ * switch (action.type) {
155
+ * case 'UPDATE_USER_NAME':
156
+ * // Must create new objects, can't mutate
157
+ * return castDeepReadonly({
158
+ * ...state,
159
+ * user: {
160
+ * ...state.user,
161
+ * profile: {
162
+ * ...state.user.profile,
163
+ * name: action.payload
164
+ * }
165
+ * }
166
+ * });
167
+ * default:
168
+ * return state;
169
+ * }
170
+ * }
171
+ * ```
172
+ *
173
+ * @example Type inference with generics
174
+ * ```typescript
175
+ * function processData<T>(data: T): DeepReadonly<T> {
176
+ * // Perform processing...
177
+ * console.log('Processing:', data);
178
+ *
179
+ * // Return immutable version
180
+ * return castDeepReadonly(data);
181
+ * }
182
+ *
183
+ * const result = processData({ nested: { value: [1, 2, 3] } });
184
+ * // Type of result is DeepReadonly<{ nested: { value: number[] } }>
185
+ * ```
186
+ *
187
+ * @see castReadonly - For shallow readonly casting
188
+ * @see castDeepMutable - For the opposite operation (use with extreme caution)
189
+ */
190
+ export const castDeepReadonly = <T,>(mutable: T): DeepReadonly<T> =>
191
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
192
+ mutable as DeepReadonly<T>;
@@ -0,0 +1,89 @@
1
+ import { castDeepReadonly, castReadonly } from './cast-readonly.mjs';
2
+
3
+ describe('castReadonly', () => {
4
+ test('should cast mutable array to readonly', () => {
5
+ const mutableArr = [1, 2, 3];
6
+ const readonlyArr = castReadonly(mutableArr);
7
+
8
+ expect(readonlyArr).toBe(mutableArr); // Same reference
9
+ expect(readonlyArr).toStrictEqual([1, 2, 3]);
10
+ });
11
+
12
+ test('should cast mutable object to readonly', () => {
13
+ const mutableObj = { x: 1, y: 2 };
14
+ const readonlyObj = castReadonly(mutableObj);
15
+
16
+ expect(readonlyObj).toBe(mutableObj); // Same reference
17
+ expect(readonlyObj).toStrictEqual({ x: 1, y: 2 });
18
+ });
19
+
20
+ test('should preserve the runtime value', () => {
21
+ const original = { value: 42 };
22
+ const readonly = castReadonly(original);
23
+
24
+ expect(readonly.value).toBe(42);
25
+ expect(Object.is(readonly, original)).toBe(true);
26
+ });
27
+
28
+ test('should work with primitives', () => {
29
+ expect(castReadonly(42)).toBe(42);
30
+ expect(castReadonly('hello')).toBe('hello');
31
+ expect(castReadonly(true)).toBe(true);
32
+ });
33
+
34
+ test('should work with null and undefined', () => {
35
+ expect(castReadonly(null)).toBe(null);
36
+ // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
37
+ expect(castReadonly(undefined)).toBe(undefined);
38
+ });
39
+ });
40
+
41
+ describe('castDeepReadonly', () => {
42
+ test('should cast deeply nested structure to readonly', () => {
43
+ const mutableNested = {
44
+ a: { b: [1, 2, 3] },
45
+ c: { d: { e: 'value' } },
46
+ };
47
+ const readonlyNested = castDeepReadonly(mutableNested);
48
+
49
+ expect(readonlyNested).toBe(mutableNested); // Same reference
50
+ expect(readonlyNested.a.b).toStrictEqual([1, 2, 3]);
51
+ expect(readonlyNested.c.d.e).toBe('value');
52
+ });
53
+
54
+ test('should preserve runtime value for complex structures', () => {
55
+ const complex = {
56
+ users: [{ id: 1, profile: { name: 'Alice' } }],
57
+ settings: { theme: 'dark', options: { debug: true } },
58
+ };
59
+ const readonly = castDeepReadonly(complex);
60
+
61
+ expect(readonly).toBe(complex);
62
+ expect(readonly.users[0]?.profile.name).toBe('Alice');
63
+ expect(readonly.settings.options.debug).toBe(true);
64
+ });
65
+
66
+ test('should work with arrays of objects', () => {
67
+ const data = [
68
+ { id: 1, meta: { active: true } },
69
+ { id: 2, meta: { active: false } },
70
+ ];
71
+ const readonly = castDeepReadonly(data);
72
+
73
+ expect(readonly).toBe(data);
74
+ expect(readonly[0]?.meta.active).toBe(true);
75
+ expect(readonly[1]?.meta.active).toBe(false);
76
+ });
77
+
78
+ test('should work with primitives', () => {
79
+ expect(castDeepReadonly(42)).toBe(42);
80
+ expect(castDeepReadonly('hello')).toBe('hello');
81
+ expect(castDeepReadonly(true)).toBe(true);
82
+ });
83
+
84
+ test('should work with null and undefined', () => {
85
+ expect(castDeepReadonly(null)).toBe(null);
86
+ // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression
87
+ expect(castDeepReadonly(undefined)).toBe(undefined);
88
+ });
89
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Implements the logical implication (if-then) operator.
3
+ *
4
+ * Returns `true` if the antecedent is `false` or the consequent is `true`.
5
+ * In logical terms: `antecedent → consequent` is equivalent to `¬antecedent ∨ consequent`.
6
+ *
7
+ * **Truth table:**
8
+ * - `true → true` = `true` (valid implication)
9
+ * - `true → false` = `false` (invalid implication)
10
+ * - `false → true` = `true` (vacuously true)
11
+ * - `false → false` = `true` (vacuously true)
12
+ *
13
+ * @param antecedent - The condition (if part)
14
+ * @param consequent - The result that should hold if the condition is true (then part)
15
+ * @returns `true` if the implication holds, `false` otherwise
16
+ *
17
+ * @example Basic truth table demonstration
18
+ * ```typescript
19
+ * ifThen(true, true); // true (if true then true = true)
20
+ * ifThen(true, false); // false (if true then false = false)
21
+ * ifThen(false, true); // true (if false then true = true - vacuously true)
22
+ * ifThen(false, false); // true (if false then false = true - vacuously true)
23
+ * ```
24
+ *
25
+ * @example Validation logic - "if required then must have value"
26
+ * ```typescript
27
+ * function validateField(value: string, isRequired: boolean): boolean {
28
+ * const hasValue = value.trim().length > 0;
29
+ * return ifThen(isRequired, hasValue);
30
+ * }
31
+ *
32
+ * validateField('hello', true); // true (required and has value)
33
+ * validateField('', true); // false (required but no value)
34
+ * validateField('', false); // true (not required, so valid)
35
+ * validateField('hello', false); // true (not required, but has value is fine)
36
+ * ```
37
+ *
38
+ * @example Access control - "if admin then has all permissions"
39
+ * ```typescript
40
+ * function checkPermission(user: User, permission: string): boolean {
41
+ * const isAdmin = user.role === 'admin';
42
+ * const hasPermission = user.permissions.includes(permission);
43
+ *
44
+ * // Admin must have all permissions
45
+ * return ifThen(isAdmin, hasPermission);
46
+ * }
47
+ *
48
+ * const adminUser = { role: 'admin', permissions: ['read', 'write'] };
49
+ * checkPermission(adminUser, 'delete'); // false (admin without delete permission = invalid)
50
+ *
51
+ * const regularUser = { role: 'user', permissions: ['read'] };
52
+ * checkPermission(regularUser, 'delete'); // true (non-admin without permission is valid)
53
+ * ```
54
+ *
55
+ * @example Contract validation - "if premium then features enabled"
56
+ * ```typescript
57
+ * interface Subscription {
58
+ * isPremium: boolean;
59
+ * features: {
60
+ * advancedAnalytics: boolean;
61
+ * unlimitedStorage: boolean;
62
+ * prioritySupport: boolean;
63
+ * };
64
+ * }
65
+ *
66
+ * function validateSubscription(sub: Subscription): boolean {
67
+ * // If premium, then all premium features must be enabled
68
+ * return ifThen(sub.isPremium,
69
+ * sub.features.advancedAnalytics &&
70
+ * sub.features.unlimitedStorage &&
71
+ * sub.features.prioritySupport
72
+ * );
73
+ * }
74
+ * ```
75
+ *
76
+ * @example Chaining multiple implications
77
+ * ```typescript
78
+ * // "If A then B" AND "If B then C"
79
+ * function validateChain(a: boolean, b: boolean, c: boolean): boolean {
80
+ * return ifThen(a, b) && ifThen(b, c);
81
+ * }
82
+ *
83
+ * validateChain(true, true, true); // true (valid chain)
84
+ * validateChain(true, false, true); // false (breaks at first implication)
85
+ * validateChain(false, false, false); // true (vacuously true chain)
86
+ * ```
87
+ *
88
+ * @example Negation patterns
89
+ * ```typescript
90
+ * // "If not expired then valid" is equivalent to "expired OR valid"
91
+ * const isExpired = Date.now() > expiryDate;
92
+ * const isValid = checkValidity();
93
+ * const result = ifThen(!isExpired, isValid);
94
+ * // Same as: isExpired || isValid
95
+ * ```
96
+ */
97
+ export const ifThen = (antecedent: boolean, consequent: boolean): boolean =>
98
+ !antecedent || consequent;
@@ -0,0 +1,75 @@
1
+ import { ifThen } from './if-then.mjs';
2
+
3
+ describe('ifThen', () => {
4
+ test('should implement logical implication truth table', () => {
5
+ // True antecedent, true consequent -> true
6
+ expect(ifThen(true, true)).toBe(true);
7
+
8
+ // True antecedent, false consequent -> false
9
+ expect(ifThen(true, false)).toBe(false);
10
+
11
+ // False antecedent, true consequent -> true (vacuously true)
12
+ expect(ifThen(false, true)).toBe(true);
13
+
14
+ // False antecedent, false consequent -> true (vacuously true)
15
+ expect(ifThen(false, false)).toBe(true);
16
+ });
17
+
18
+ test('should work for validation logic', () => {
19
+ const validateField = (value: string, isRequired: boolean): boolean => {
20
+ const hasValue = value.trim().length > 0;
21
+ return ifThen(isRequired, hasValue);
22
+ };
23
+
24
+ expect(validateField('hello', true)).toBe(true); // required and has value
25
+ expect(validateField('', true)).toBe(false); // required but no value
26
+ expect(validateField('', false)).toBe(true); // not required, so valid
27
+ expect(validateField('hello', false)).toBe(true); // not required, has value
28
+ });
29
+
30
+ test('should work for access control logic', () => {
31
+ const checkPermission = (
32
+ isAdmin: boolean,
33
+ hasPermission: boolean,
34
+ ): boolean =>
35
+ // If admin, then must have permission
36
+ ifThen(isAdmin, hasPermission);
37
+ expect(checkPermission(true, true)).toBe(true); // admin with permission
38
+ expect(checkPermission(true, false)).toBe(false); // admin without permission
39
+ expect(checkPermission(false, true)).toBe(true); // non-admin with permission
40
+ expect(checkPermission(false, false)).toBe(true); // non-admin without permission
41
+ });
42
+
43
+ test('should work for contract validation', () => {
44
+ const validateSubscription = (
45
+ isPremium: boolean,
46
+ hasAllFeatures: boolean,
47
+ ): boolean =>
48
+ // If premium, then all premium features must be enabled
49
+ ifThen(isPremium, hasAllFeatures);
50
+ expect(validateSubscription(true, true)).toBe(true); // premium with all features
51
+ expect(validateSubscription(true, false)).toBe(false); // premium without all features
52
+ expect(validateSubscription(false, true)).toBe(true); // non-premium with features
53
+ expect(validateSubscription(false, false)).toBe(true); // non-premium without features
54
+ });
55
+
56
+ test('should work in chaining scenarios', () => {
57
+ const validateChain = (a: boolean, b: boolean, c: boolean): boolean =>
58
+ ifThen(a, b) && ifThen(b, c);
59
+
60
+ expect(validateChain(true, true, true)).toBe(true); // valid chain
61
+ expect(validateChain(true, false, true)).toBe(false); // breaks at first implication
62
+ expect(validateChain(false, false, false)).toBe(true); // vacuously true chain
63
+ expect(validateChain(true, true, false)).toBe(false); // breaks at second implication
64
+ });
65
+
66
+ test('should work with negation patterns', () => {
67
+ const checkExpiredLogic = (isExpired: boolean, isValid: boolean): boolean =>
68
+ // "If not expired then valid" is equivalent to "expired OR valid"
69
+ ifThen(!isExpired, isValid);
70
+ expect(checkExpiredLogic(false, true)).toBe(true); // not expired and valid
71
+ expect(checkExpiredLogic(false, false)).toBe(false); // not expired but invalid
72
+ expect(checkExpiredLogic(true, true)).toBe(true); // expired but valid (vacuous)
73
+ expect(checkExpiredLogic(true, false)).toBe(true); // expired and invalid (vacuous)
74
+ });
75
+ });
@@ -0,0 +1,7 @@
1
+ export * from './cast-mutable.mjs';
2
+ export * from './cast-readonly.mjs';
3
+ export * from './if-then.mjs';
4
+ export * from './map-nullable.mjs';
5
+ export * from './memoize-function.mjs';
6
+ export * from './tuple.mjs';
7
+ export * from './unknown-to-string.mjs';
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Applies a function to a value if the value is not `null` or `undefined`.
3
+ * If the value is `null` or `undefined`, it returns `undefined`.
4
+ *
5
+ * This function provides a safe way to transform nullable values without explicit null checks.
6
+ * It's similar to Optional.map() but works directly with TypeScript's nullable types.
7
+ * Supports both regular and curried usage for functional composition.
8
+ *
9
+ * @template A - The type of the input value (excluding null/undefined)
10
+ * @template B - The type of the value returned by the mapping function
11
+ * @param value - The value to potentially transform (can be `A`, `null`, or `undefined`)
12
+ * @param mapFn - A function that transforms a non-nullable value of type `A` to type `B`
13
+ * @returns The result of applying `mapFn` to `value` if value is not null/undefined; otherwise `undefined`
14
+ *
15
+ * @example Basic usage with nullable values
16
+ * ```typescript
17
+ * // Safe string transformation
18
+ * mapNullable("hello", s => s.toUpperCase()); // "HELLO"
19
+ * mapNullable(null, s => s.toUpperCase()); // undefined
20
+ * mapNullable(undefined, s => s.toUpperCase()); // undefined
21
+ *
22
+ * // Number operations
23
+ * mapNullable(5, n => n * 2); // 10
24
+ * mapNullable(0, n => n * 2); // 0 (note: 0 is not null/undefined)
25
+ * mapNullable(null as number | null, n => n * 2); // undefined
26
+ * ```
27
+ *
28
+ * @example Working with optional object properties
29
+ * ```typescript
30
+ * interface User {
31
+ * id: number;
32
+ * name?: string;
33
+ * email?: string;
34
+ * }
35
+ *
36
+ * function formatUserDisplay(user: User): string {
37
+ * const displayName = mapNullable(user.name, name => name.toUpperCase()) ?? 'Anonymous';
38
+ * const emailDomain = mapNullable(user.email, email => email.split('@')[1]);
39
+ *
40
+ * return `${displayName} ${emailDomain ? `(${emailDomain})` : ''}`;
41
+ * }
42
+ *
43
+ * formatUserDisplay({ id: 1, name: 'John', email: 'john@example.com' }); // "JOHN (example.com)"
44
+ * formatUserDisplay({ id: 2 }); // "Anonymous "
45
+ * ```
46
+ *
47
+ * @example Curried usage for functional composition
48
+ * ```typescript
49
+ * // Create reusable transformers
50
+ * const toUpperCase = mapNullable((s: string) => s.toUpperCase());
51
+ * const addPrefix = mapNullable((s: string) => `PREFIX_${s}`);
52
+ * const parseNumber = mapNullable((s: string) => parseInt(s, 10));
53
+ *
54
+ * // Use in different contexts
55
+ * toUpperCase("hello"); // "HELLO"
56
+ * toUpperCase(null); // undefined
57
+ *
58
+ * // Compose transformations
59
+ * const processString = (s: string | null) => {
60
+ * const upper = toUpperCase(s);
61
+ * return addPrefix(upper);
62
+ * };
63
+ *
64
+ * processString("test"); // "PREFIX_TEST"
65
+ * processString(null); // undefined
66
+ * ```
67
+ *
68
+ * @example Chaining nullable operations
69
+ * ```typescript
70
+ * // API response handling
71
+ * interface ApiResponse {
72
+ * data?: {
73
+ * user?: {
74
+ * profile?: {
75
+ * displayName?: string;
76
+ * };
77
+ * };
78
+ * };
79
+ * }
80
+ *
81
+ * function getDisplayName(response: ApiResponse): string | undefined {
82
+ * return mapNullable(
83
+ * response.data?.user?.profile?.displayName,
84
+ * name => name.trim().toUpperCase()
85
+ * );
86
+ * }
87
+ *
88
+ * // Chain multiple transformations
89
+ * function processNullableChain(value: string | null): string | undefined {
90
+ * const step1 = mapNullable(value, v => v.trim());
91
+ * const step2 = mapNullable(step1, v => v.length > 0 ? v : null);
92
+ * const step3 = mapNullable(step2, v => v.toUpperCase());
93
+ * return step3;
94
+ * }
95
+ * ```
96
+ *
97
+ * @example Integration with array methods
98
+ * ```typescript
99
+ * const nullableNumbers: (number | null | undefined)[] = [1, null, 3, undefined, 5];
100
+ *
101
+ * // Transform and filter in one step
102
+ * const doubled = nullableNumbers
103
+ * .map(n => mapNullable(n, x => x * 2))
104
+ * .filter((n): n is number => n !== undefined);
105
+ * // Result: [2, 6, 10]
106
+ *
107
+ * // Process optional array elements
108
+ * const users: Array<{ name?: string }> = [
109
+ * { name: 'Alice' },
110
+ * { name: undefined },
111
+ * { name: 'Bob' }
112
+ * ];
113
+ *
114
+ * const upperNames = users
115
+ * .map(u => mapNullable(u.name, n => n.toUpperCase()))
116
+ * .filter((n): n is string => n !== undefined);
117
+ * // Result: ['ALICE', 'BOB']
118
+ * ```
119
+ *
120
+ * @example Error handling patterns
121
+ * ```typescript
122
+ * // Safe JSON parsing
123
+ * function parseJsonSafe<T>(json: string | null): T | undefined {
124
+ * return mapNullable(json, j => {
125
+ * try {
126
+ * return JSON.parse(j) as T;
127
+ * } catch {
128
+ * return null;
129
+ * }
130
+ * }) ?? undefined;
131
+ * }
132
+ *
133
+ * // Safe property access with computation
134
+ * function calculateAge(birthYear: number | null): string | undefined {
135
+ * return mapNullable(birthYear, year => {
136
+ * const age = new Date().getFullYear() - year;
137
+ * return age >= 0 ? `${age} years old` : null;
138
+ * }) ?? undefined;
139
+ * }
140
+ * ```
141
+ *
142
+ * @see Optional - For more complex optional value handling
143
+ * @see Result - For error handling with detailed error information
144
+ */
145
+ export const mapNullable: MapNullableFnOverload = (<const A, const B>(
146
+ ...args:
147
+ | readonly [value: A | null | undefined, mapFn: (v: A) => B]
148
+ | readonly [mapFn: (v: A) => B]
149
+ ): (B | undefined) | ((value: A | null | undefined) => B | undefined) => {
150
+ switch (args.length) {
151
+ case 2: {
152
+ const [value, mapFn] = args;
153
+ return value == null ? undefined : mapFn(value);
154
+ }
155
+ case 1: {
156
+ const [mapFn] = args;
157
+ return (value: A | null | undefined) => mapNullable(value, mapFn);
158
+ }
159
+ }
160
+ }) as MapNullableFnOverload;
161
+
162
+ type MapNullableFnOverload = {
163
+ <const A, const B>(
164
+ value: A | null | undefined,
165
+ mapFn: (v: A) => B,
166
+ ): B | undefined;
167
+
168
+ // Curried version
169
+ <const A, const B>(
170
+ mapFn: (v: A) => B,
171
+ ): (value: A | null | undefined) => B | undefined;
172
+ };