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,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,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
|
+
};
|