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,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a readonly tuple from the given arguments with precise literal type inference.
|
|
3
|
+
*
|
|
4
|
+
* This utility function provides a concise way to create tuples while preserving
|
|
5
|
+
* exact literal types. Without this function, TypeScript would infer arrays with
|
|
6
|
+
* widened types instead of tuples with literal types.
|
|
7
|
+
*
|
|
8
|
+
* **Key benefits:**
|
|
9
|
+
* - Preserves literal types (e.g., `1` instead of `number`)
|
|
10
|
+
* - Creates readonly tuples for immutability
|
|
11
|
+
* - Provides better type inference than array literals
|
|
12
|
+
* - Zero runtime overhead - just returns the arguments
|
|
13
|
+
*
|
|
14
|
+
* @template T - A tuple type with literal types inferred from the arguments
|
|
15
|
+
* @param args - The elements to include in the tuple (variadic)
|
|
16
|
+
* @returns A readonly tuple containing the provided arguments with preserved literal types
|
|
17
|
+
*
|
|
18
|
+
* @example Basic tuple creation with literal types
|
|
19
|
+
* ```typescript
|
|
20
|
+
* // Without tp: types are widened
|
|
21
|
+
* const arr = [1, 'hello', true]; // (string | number | boolean)[]
|
|
22
|
+
*
|
|
23
|
+
* // With tp: exact literal types preserved
|
|
24
|
+
* const tuple = tp(1, 'hello', true); // readonly [1, 'hello', true]
|
|
25
|
+
* const coords = tp(10, 20); // readonly [10, 20]
|
|
26
|
+
* const single = tp('only'); // readonly ['only']
|
|
27
|
+
* const empty = tp(); // readonly []
|
|
28
|
+
*
|
|
29
|
+
* // TypeScript knows exact values at compile time
|
|
30
|
+
* type First = typeof tuple[0]; // 1 (literal type)
|
|
31
|
+
* type Second = typeof tuple[1]; // 'hello' (literal type)
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example Creating type-safe coordinate systems
|
|
35
|
+
* ```typescript
|
|
36
|
+
* // 2D coordinates
|
|
37
|
+
* const point2D = tp(10, 20);
|
|
38
|
+
* const [x, y] = point2D; // x: 10, y: 20
|
|
39
|
+
*
|
|
40
|
+
* // 3D coordinates
|
|
41
|
+
* const point3D = tp(10, 20, 30);
|
|
42
|
+
* const [x3, y3, z3] = point3D; // Exact types preserved
|
|
43
|
+
*
|
|
44
|
+
* // Named coordinate system
|
|
45
|
+
* const namedPoint = tp('x', 100, 'y', 200);
|
|
46
|
+
* // Type: readonly ['x', 100, 'y', 200]
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* @example Building type-safe data structures
|
|
50
|
+
* ```typescript
|
|
51
|
+
* // Creating a type-safe map structure
|
|
52
|
+
* const colorMap = [
|
|
53
|
+
* tp('red', '#FF0000'),
|
|
54
|
+
* tp('green', '#00FF00'),
|
|
55
|
+
* tp('blue', '#0000FF')
|
|
56
|
+
* ] as const;
|
|
57
|
+
* // Type: readonly [readonly ['red', '#FF0000'], ...]
|
|
58
|
+
*
|
|
59
|
+
* // Type-safe event system
|
|
60
|
+
* type EventTuple = readonly ['click', MouseEvent] | readonly ['change', Event];
|
|
61
|
+
* const event = tp('click', new MouseEvent('click')) as EventTuple;
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example Function argument patterns
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // Functions expecting exact tuple types
|
|
67
|
+
* function processCoordinate(coord: readonly [number, number]): void {
|
|
68
|
+
* const [x, y] = coord;
|
|
69
|
+
* console.log(`Processing point at (${x}, ${y})`);
|
|
70
|
+
* }
|
|
71
|
+
*
|
|
72
|
+
* processCoordinate(tp(10, 20)); // ✅ Type-safe
|
|
73
|
+
* processCoordinate([10, 20]); // ❌ Error: number[] not assignable
|
|
74
|
+
*
|
|
75
|
+
* // Pattern matching with tuples
|
|
76
|
+
* function handleMessage(msg: readonly ['error', string] | readonly ['success', any]) {
|
|
77
|
+
* if (msg[0] === 'error') {
|
|
78
|
+
* console.error(msg[1]); // TypeScript knows msg[1] is string
|
|
79
|
+
* } else {
|
|
80
|
+
* console.log('Success:', msg[1]);
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* handleMessage(tp('error', 'Failed to load'));
|
|
85
|
+
* handleMessage(tp('success', { id: 123 }));
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example Advanced type inference
|
|
89
|
+
* ```typescript
|
|
90
|
+
* // Const assertions comparison
|
|
91
|
+
* const tuple1 = [1, 2, 3]; // number[]
|
|
92
|
+
* const tuple2 = [1, 2, 3] as const; // readonly [1, 2, 3]
|
|
93
|
+
* const tuple3 = tp(1, 2, 3); // readonly [1, 2, 3]
|
|
94
|
+
*
|
|
95
|
+
* // Building complex types
|
|
96
|
+
* const config = tp(
|
|
97
|
+
* tp('host', 'localhost'),
|
|
98
|
+
* tp('port', 3000),
|
|
99
|
+
* tp('secure', true)
|
|
100
|
+
* );
|
|
101
|
+
* // Type: readonly [
|
|
102
|
+
* // readonly ['host', 'localhost'],
|
|
103
|
+
* // readonly ['port', 3000],
|
|
104
|
+
* // readonly ['secure', true]
|
|
105
|
+
* // ]
|
|
106
|
+
*
|
|
107
|
+
* // Type-safe destructuring
|
|
108
|
+
* const [[, host], [, port], [, secure]] = config;
|
|
109
|
+
* // host: 'localhost', port: 3000, secure: true
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* @example Integration with other utilities
|
|
113
|
+
* ```typescript
|
|
114
|
+
* import { pipe } from '../functional/pipe';
|
|
115
|
+
* import { Result } from '../functional/result';
|
|
116
|
+
*
|
|
117
|
+
* // Type-safe error handling
|
|
118
|
+
* function divide(a: number, b: number): Result<number, string> {
|
|
119
|
+
* if (b === 0) return Result.err('Division by zero');
|
|
120
|
+
* return Result.ok(a / b);
|
|
121
|
+
* }
|
|
122
|
+
*
|
|
123
|
+
* const calculation = tp(10, 2);
|
|
124
|
+
* const result = divide(...calculation); // Spread tuple as arguments
|
|
125
|
+
*
|
|
126
|
+
* // Building pipelines with tuples
|
|
127
|
+
* const pipeline = pipe(tp(5, 10))
|
|
128
|
+
* .map(([a, b]) => tp(a + b, a * b))
|
|
129
|
+
* .map(([sum, product]) => tp('sum', sum, 'product', product))
|
|
130
|
+
* .value;
|
|
131
|
+
* // Type: readonly ['sum', 15, 'product', 50]
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* @example Common patterns and use cases
|
|
135
|
+
* ```typescript
|
|
136
|
+
* // React-style state tuples
|
|
137
|
+
* const useState = <T>(initial: T) => tp(initial, (value: T) => void 0);
|
|
138
|
+
* const [count, setCount] = useState(0);
|
|
139
|
+
*
|
|
140
|
+
* // Redux-style actions
|
|
141
|
+
* const incrementAction = tp('INCREMENT', { amount: 1 });
|
|
142
|
+
* const decrementAction = tp('DECREMENT', { amount: 1 });
|
|
143
|
+
*
|
|
144
|
+
* // Database query results
|
|
145
|
+
* const queryResult = tp(
|
|
146
|
+
* true, // success
|
|
147
|
+
* [{ id: 1, name: 'John' }], // data
|
|
148
|
+
* null // error
|
|
149
|
+
* );
|
|
150
|
+
*
|
|
151
|
+
* // Configuration flags
|
|
152
|
+
* const features = tp('darkMode', 'analytics', 'notifications');
|
|
153
|
+
* const enabledFeatures = features.filter(f => isEnabled(f));
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* @see https://www.typescriptlang.org/docs/handbook/2/objects.html#tuple-types
|
|
157
|
+
*/
|
|
158
|
+
export const tp = <const T extends readonly unknown[]>(
|
|
159
|
+
...args: T
|
|
160
|
+
): Readonly<T> => args;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { tp } from './tuple.mjs';
|
|
3
|
+
|
|
4
|
+
describe('tp', () => {
|
|
5
|
+
test('test type', () => {
|
|
6
|
+
const tuple = tp(1, 2, 3);
|
|
7
|
+
expect(tuple).toStrictEqual([1, 2, 3]);
|
|
8
|
+
|
|
9
|
+
expectType<typeof tuple, readonly [1, 2, 3]>('=');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Result } from '../functional/index.mjs';
|
|
2
|
+
import { isNonNullish } from '../guard/index.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts an unknown value to its string representation in a type-safe manner.
|
|
6
|
+
*
|
|
7
|
+
* This function handles all JavaScript types and provides consistent string conversion
|
|
8
|
+
* with proper error handling for edge cases like circular references. Unlike naive
|
|
9
|
+
* toString() calls, this function never throws and handles all value types gracefully.
|
|
10
|
+
*
|
|
11
|
+
* **Type conversion rules:**
|
|
12
|
+
* - Strings: returned as-is
|
|
13
|
+
* - Numbers, booleans, bigints: converted via toString()
|
|
14
|
+
* - Symbols: converted to their description string
|
|
15
|
+
* - Functions: converted to their string representation
|
|
16
|
+
* - null: returns "null" (not "null" from JSON)
|
|
17
|
+
* - undefined: returns "undefined"
|
|
18
|
+
* - Objects: JSON stringified (with optional pretty printing)
|
|
19
|
+
*
|
|
20
|
+
* @param value - The unknown value to convert to string
|
|
21
|
+
* @param options - Optional configuration for the conversion
|
|
22
|
+
* @param options.prettyPrintObject - If true, objects are formatted with 2-space indentation
|
|
23
|
+
* @returns A Result containing either the string representation or an Error for failures
|
|
24
|
+
*
|
|
25
|
+
* @example Basic type conversions
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Primitive types
|
|
28
|
+
* unknownToString('hello'); // Ok('hello')
|
|
29
|
+
* unknownToString(123); // Ok('123')
|
|
30
|
+
* unknownToString(true); // Ok('true')
|
|
31
|
+
* unknownToString(null); // Ok('null')
|
|
32
|
+
* unknownToString(undefined); // Ok('undefined')
|
|
33
|
+
* unknownToString(Symbol('test')); // Ok('Symbol(test)')
|
|
34
|
+
* unknownToString(123n); // Ok('123')
|
|
35
|
+
*
|
|
36
|
+
* // Function conversion
|
|
37
|
+
* const fn = () => 'test';
|
|
38
|
+
* unknownToString(fn); // Ok('() => 'test'')
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* @example Object stringification
|
|
42
|
+
* ```typescript
|
|
43
|
+
* // Simple object
|
|
44
|
+
* const obj = { a: 1, b: 'hello', c: [1, 2, 3] };
|
|
45
|
+
* const result = unknownToString(obj);
|
|
46
|
+
* if (Result.isOk(result)) {
|
|
47
|
+
* console.log(result.value); // '{"a":1,"b":"hello","c":[1,2,3]}'
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* // Pretty printing
|
|
51
|
+
* const prettyResult = unknownToString(obj, { prettyPrintObject: true });
|
|
52
|
+
* if (Result.isOk(prettyResult)) {
|
|
53
|
+
* console.log(prettyResult.value);
|
|
54
|
+
* // {
|
|
55
|
+
* // "a": 1,
|
|
56
|
+
* // "b": "hello",
|
|
57
|
+
* // "c": [
|
|
58
|
+
* // 1,
|
|
59
|
+
* // 2,
|
|
60
|
+
* // 3
|
|
61
|
+
* // ]
|
|
62
|
+
* // }
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* @example Error handling for circular references
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Circular reference
|
|
69
|
+
* const circular: any = { name: 'parent' };
|
|
70
|
+
* circular.self = circular;
|
|
71
|
+
*
|
|
72
|
+
* const result = unknownToString(circular);
|
|
73
|
+
* if (Result.isErr(result)) {
|
|
74
|
+
* console.log(result.value.message);
|
|
75
|
+
* // "Converting circular structure to JSON"
|
|
76
|
+
* }
|
|
77
|
+
*
|
|
78
|
+
* // Handle with custom serialization
|
|
79
|
+
* const safeStringify = (value: unknown): string => {
|
|
80
|
+
* const result = unknownToString(value);
|
|
81
|
+
* return Result.isOk(result)
|
|
82
|
+
* ? result.value
|
|
83
|
+
* : `[Error: ${result.value.message}]`;
|
|
84
|
+
* };
|
|
85
|
+
* ```
|
|
86
|
+
*
|
|
87
|
+
* @example Logging and debugging utilities
|
|
88
|
+
* ```typescript
|
|
89
|
+
* // Type-safe logger
|
|
90
|
+
* class Logger {
|
|
91
|
+
* log(message: string, data?: unknown): void {
|
|
92
|
+
* const timestamp = new Date().toISOString();
|
|
93
|
+
* const dataStr = data !== undefined
|
|
94
|
+
* ? unknownToString(data, { prettyPrintObject: true })
|
|
95
|
+
* : Result.ok('');
|
|
96
|
+
*
|
|
97
|
+
* if (Result.isOk(dataStr)) {
|
|
98
|
+
* console.log(`[${timestamp}] ${message}`, dataStr.value);
|
|
99
|
+
* } else {
|
|
100
|
+
* console.log(`[${timestamp}] ${message} [Unstringifiable data]`);
|
|
101
|
+
* }
|
|
102
|
+
* }
|
|
103
|
+
* }
|
|
104
|
+
*
|
|
105
|
+
* const logger = new Logger();
|
|
106
|
+
* logger.log('User data:', { id: 123, name: 'John' });
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @example API response formatting
|
|
110
|
+
* ```typescript
|
|
111
|
+
* // Safe error response formatting
|
|
112
|
+
* function formatErrorResponse(error: unknown): string {
|
|
113
|
+
* const result = unknownToString(error, { prettyPrintObject: true });
|
|
114
|
+
*
|
|
115
|
+
* if (Result.isOk(result)) {
|
|
116
|
+
* return JSON.stringify({
|
|
117
|
+
* success: false,
|
|
118
|
+
* error: result.value
|
|
119
|
+
* });
|
|
120
|
+
* }
|
|
121
|
+
*
|
|
122
|
+
* // Fallback for unstringifiable errors
|
|
123
|
+
* return JSON.stringify({
|
|
124
|
+
* success: false,
|
|
125
|
+
* error: 'An unknown error occurred'
|
|
126
|
+
* });
|
|
127
|
+
* }
|
|
128
|
+
*
|
|
129
|
+
* try {
|
|
130
|
+
* // some operation
|
|
131
|
+
* } catch (error) {
|
|
132
|
+
* const response = formatErrorResponse(error);
|
|
133
|
+
* res.status(500).send(response);
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @example Working with special objects
|
|
138
|
+
* ```typescript
|
|
139
|
+
* // Date objects
|
|
140
|
+
* unknownToString(new Date('2023-01-01'));
|
|
141
|
+
* // Ok('"2023-01-01T00:00:00.000Z"') - JSON stringified
|
|
142
|
+
*
|
|
143
|
+
* // Regular expressions
|
|
144
|
+
* unknownToString(/test/gi);
|
|
145
|
+
* // Ok('{}') - RegExp has no enumerable properties
|
|
146
|
+
*
|
|
147
|
+
* // Arrays
|
|
148
|
+
* unknownToString([1, 'two', { three: 3 }]);
|
|
149
|
+
* // Ok('[1,"two",{"three":3}]')
|
|
150
|
+
*
|
|
151
|
+
* // Map and Set (converted to empty objects by JSON.stringify)
|
|
152
|
+
* unknownToString(new Map([['a', 1]])); // Ok('{}')
|
|
153
|
+
* unknownToString(new Set([1, 2, 3])); // Ok('{}')
|
|
154
|
+
* ```
|
|
155
|
+
*
|
|
156
|
+
* @example Integration with Result type
|
|
157
|
+
* ```typescript
|
|
158
|
+
* import { Result, pipe } from '../functional';
|
|
159
|
+
*
|
|
160
|
+
* // Chain with other Result operations
|
|
161
|
+
* function processUserInput(input: unknown): Result<string, Error> {
|
|
162
|
+
* return pipe(input)
|
|
163
|
+
* .map(val => unknownToString(val))
|
|
164
|
+
* .map(Result.flatten) // Flatten Result<Result<string, Error>, never>
|
|
165
|
+
* .map(str => Result.map(str, s => s.trim()))
|
|
166
|
+
* .map(Result.flatten)
|
|
167
|
+
* .map(str => Result.flatMap(str, s =>
|
|
168
|
+
* s.length > 0
|
|
169
|
+
* ? Result.ok(s)
|
|
170
|
+
* : Result.err(new Error('Empty string'))
|
|
171
|
+
* ))
|
|
172
|
+
* .value;
|
|
173
|
+
* }
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* @see Result - For error handling pattern used by this function
|
|
177
|
+
* @see JSON.stringify - Underlying serialization for objects
|
|
178
|
+
*/
|
|
179
|
+
export const unknownToString = (
|
|
180
|
+
value: unknown,
|
|
181
|
+
options?: Partial<Readonly<{ prettyPrintObject: boolean }>>,
|
|
182
|
+
): Result<string, Error> => {
|
|
183
|
+
switch (typeof value) {
|
|
184
|
+
case 'string':
|
|
185
|
+
return Result.ok(value);
|
|
186
|
+
|
|
187
|
+
case 'number':
|
|
188
|
+
case 'bigint':
|
|
189
|
+
case 'boolean':
|
|
190
|
+
case 'symbol':
|
|
191
|
+
case 'function':
|
|
192
|
+
return Result.ok(value.toString());
|
|
193
|
+
|
|
194
|
+
case 'object':
|
|
195
|
+
if (!isNonNullish(value)) {
|
|
196
|
+
return Result.ok('null');
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
const stringified =
|
|
200
|
+
options?.prettyPrintObject === true
|
|
201
|
+
? JSON.stringify(value, undefined, 2)
|
|
202
|
+
: JSON.stringify(value);
|
|
203
|
+
return Result.ok(stringified);
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return Result.err(
|
|
206
|
+
error instanceof Error
|
|
207
|
+
? error
|
|
208
|
+
: new Error('Failed to stringify object'),
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case 'undefined':
|
|
213
|
+
return Result.ok('undefined');
|
|
214
|
+
}
|
|
215
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Result } from '../functional/index.mjs';
|
|
2
|
+
import { unknownToString } from './unknown-to-string.mjs';
|
|
3
|
+
|
|
4
|
+
describe('unknownToString', () => {
|
|
5
|
+
test('string', () => {
|
|
6
|
+
const result = unknownToString('aaaaa');
|
|
7
|
+
expect(Result.isOk(result)).toBe(true);
|
|
8
|
+
if (Result.isOk(result)) {
|
|
9
|
+
expect(result.value).toBe('aaaaa');
|
|
10
|
+
}
|
|
11
|
+
expect(JSON.stringify('aaaaa')).toBe('"aaaaa"');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('number', () => {
|
|
15
|
+
const result = unknownToString(1);
|
|
16
|
+
expect(Result.isOk(result)).toBe(true);
|
|
17
|
+
if (Result.isOk(result)) {
|
|
18
|
+
expect(result.value).toBe('1');
|
|
19
|
+
}
|
|
20
|
+
expect(JSON.stringify(1)).toBe('1');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('boolean', () => {
|
|
24
|
+
const result = unknownToString(true);
|
|
25
|
+
expect(Result.isOk(result)).toBe(true);
|
|
26
|
+
if (Result.isOk(result)) {
|
|
27
|
+
expect(result.value).toBe('true');
|
|
28
|
+
}
|
|
29
|
+
expect(JSON.stringify(true)).toBe('true');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('symbol', () => {
|
|
33
|
+
const result = unknownToString(Symbol('sym'));
|
|
34
|
+
expect(Result.isOk(result)).toBe(true);
|
|
35
|
+
if (Result.isOk(result)) {
|
|
36
|
+
expect(result.value).toBe('Symbol(sym)');
|
|
37
|
+
}
|
|
38
|
+
expect(JSON.stringify(Symbol('sym'))).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('function', () => {
|
|
42
|
+
const result = unknownToString(() => 0);
|
|
43
|
+
expect(Result.isOk(result)).toBe(true);
|
|
44
|
+
if (Result.isOk(result)) {
|
|
45
|
+
expect(result.value).toBe('() => 0');
|
|
46
|
+
}
|
|
47
|
+
expect(JSON.stringify(() => 0)).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('undefined', () => {
|
|
51
|
+
const result = unknownToString(undefined);
|
|
52
|
+
expect(Result.isOk(result)).toBe(true);
|
|
53
|
+
if (Result.isOk(result)) {
|
|
54
|
+
expect(result.value).toBe('undefined');
|
|
55
|
+
}
|
|
56
|
+
expect(JSON.stringify(undefined)).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('null', () => {
|
|
60
|
+
const result = unknownToString(null);
|
|
61
|
+
expect(Result.isOk(result)).toBe(true);
|
|
62
|
+
if (Result.isOk(result)) {
|
|
63
|
+
expect(result.value).toBe('null');
|
|
64
|
+
}
|
|
65
|
+
expect(JSON.stringify(null)).toBe('null');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('object', () => {
|
|
69
|
+
const result = unknownToString({ a: { b: 1 } });
|
|
70
|
+
expect(Result.isOk(result)).toBe(true);
|
|
71
|
+
if (Result.isOk(result)) {
|
|
72
|
+
expect(result.value).toBe('{"a":{"b":1}}');
|
|
73
|
+
}
|
|
74
|
+
expect(JSON.stringify({ a: { b: 1 } })).toBe('{"a":{"b":1}}');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('object(prettyPrint=true)', () => {
|
|
78
|
+
const result = unknownToString(
|
|
79
|
+
{ a: { b: 1 } },
|
|
80
|
+
{ prettyPrintObject: true },
|
|
81
|
+
);
|
|
82
|
+
expect(Result.isOk(result)).toBe(true);
|
|
83
|
+
if (Result.isOk(result)) {
|
|
84
|
+
expect(result.value).toBe(
|
|
85
|
+
[
|
|
86
|
+
//
|
|
87
|
+
`{`,
|
|
88
|
+
` "a": {`,
|
|
89
|
+
` "b": 1`,
|
|
90
|
+
` }`,
|
|
91
|
+
`}`,
|
|
92
|
+
].join('\n'),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('circular reference returns error', () => {
|
|
98
|
+
const circular: { a: number; self?: unknown } = { a: 1 };
|
|
99
|
+
circular.self = circular;
|
|
100
|
+
const result = unknownToString(circular);
|
|
101
|
+
expect(Result.isErr(result)).toBe(true);
|
|
102
|
+
if (Result.isErr(result)) {
|
|
103
|
+
expect(result.value.message).toContain('circular');
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('BigInt value', () => {
|
|
108
|
+
const result = unknownToString(BigInt(123));
|
|
109
|
+
expect(Result.isOk(result)).toBe(true);
|
|
110
|
+
if (Result.isOk(result)) {
|
|
111
|
+
expect(result.value).toBe('123');
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
});
|