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,90 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { isNonNullObject } from './is-non-null-object.mjs';
|
|
3
|
+
|
|
4
|
+
describe('isNonNullObject', () => {
|
|
5
|
+
test('should return true for plain objects', () => {
|
|
6
|
+
expect(isNonNullObject({})).toBe(true);
|
|
7
|
+
expect(isNonNullObject({ a: 1, b: 'test' })).toBe(true);
|
|
8
|
+
expect(isNonNullObject({ nested: { value: true } })).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should return true for arrays', () => {
|
|
12
|
+
expect(isNonNullObject([])).toBe(true);
|
|
13
|
+
expect(isNonNullObject([1, 2, 3])).toBe(true);
|
|
14
|
+
expect(isNonNullObject(['a', 'b', 'c'])).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should return true for functions', () => {
|
|
18
|
+
expect(isNonNullObject(() => {})).toBe(false);
|
|
19
|
+
expect(isNonNullObject(async () => {})).toBe(false);
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
|
|
21
|
+
expect(isNonNullObject(class MyClass {})).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should return true for built-in objects', () => {
|
|
25
|
+
expect(isNonNullObject(new Date())).toBe(true);
|
|
26
|
+
expect(isNonNullObject(/test/u)).toBe(true);
|
|
27
|
+
expect(isNonNullObject(/regex/u)).toBe(true);
|
|
28
|
+
expect(isNonNullObject(new Map())).toBe(true);
|
|
29
|
+
expect(isNonNullObject(new Set())).toBe(true);
|
|
30
|
+
expect(isNonNullObject(new Error('test'))).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should return true for boxed primitives', () => {
|
|
34
|
+
// eslint-disable-next-line no-new-wrappers
|
|
35
|
+
expect(isNonNullObject(new String('hello'))).toBe(true);
|
|
36
|
+
// eslint-disable-next-line no-new-wrappers
|
|
37
|
+
expect(isNonNullObject(new Number(42))).toBe(true);
|
|
38
|
+
// eslint-disable-next-line no-new-wrappers
|
|
39
|
+
expect(isNonNullObject(new Boolean(true))).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should return false for null', () => {
|
|
43
|
+
expect(isNonNullObject(null)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should return false for primitive values', () => {
|
|
47
|
+
expect(isNonNullObject(undefined)).toBe(false);
|
|
48
|
+
expect(isNonNullObject('string')).toBe(false);
|
|
49
|
+
expect(isNonNullObject(42)).toBe(false);
|
|
50
|
+
expect(isNonNullObject(true)).toBe(false);
|
|
51
|
+
expect(isNonNullObject(false)).toBe(false);
|
|
52
|
+
expect(isNonNullObject(Symbol('test'))).toBe(false);
|
|
53
|
+
expect(isNonNullObject(BigInt(123))).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should act as a type guard', () => {
|
|
57
|
+
const value: unknown = { test: true };
|
|
58
|
+
|
|
59
|
+
if (isNonNullObject(value)) {
|
|
60
|
+
expectType<typeof value, NonNullable<UnknownRecord>>('>=');
|
|
61
|
+
// Can access object methods
|
|
62
|
+
expect(typeof value.toString).toBe('function');
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should narrow nullable object types', () => {
|
|
67
|
+
const nullable: UnknownRecord | null = Math.random() > 0.5 ? {} : null;
|
|
68
|
+
|
|
69
|
+
if (isNonNullObject(nullable)) {
|
|
70
|
+
expectType<typeof nullable, UnknownRecord>('=');
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('should work in filter operations', () => {
|
|
75
|
+
const mixed: unknown[] = [
|
|
76
|
+
'string',
|
|
77
|
+
42,
|
|
78
|
+
{ a: 1 },
|
|
79
|
+
null,
|
|
80
|
+
undefined,
|
|
81
|
+
[],
|
|
82
|
+
() => {},
|
|
83
|
+
true,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const objects = mixed.filter(isNonNullObject);
|
|
87
|
+
|
|
88
|
+
expect(objects).toStrictEqual([{ a: 1 }, []]);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard that checks if a value is a primitive type.
|
|
5
|
+
*
|
|
6
|
+
* This function identifies JavaScript primitive types, which are immutable data types that are
|
|
7
|
+
* not objects. The primitive types are: `string`, `number`, `boolean`, `undefined`, `symbol`,
|
|
8
|
+
* `bigint`, and `null`.
|
|
9
|
+
*
|
|
10
|
+
* **Important Note:** Although `null` has `typeof null === "object"` due to a historical
|
|
11
|
+
* JavaScript quirk, this function correctly identifies `null` as a primitive value.
|
|
12
|
+
*
|
|
13
|
+
* **Type Narrowing Behavior:**
|
|
14
|
+
* - Narrows the input type to `Primitive` (union of all primitive types)
|
|
15
|
+
* - Excludes object types, arrays, functions, and other non-primitive values
|
|
16
|
+
* - Includes `null` despite its misleading `typeof` result
|
|
17
|
+
*
|
|
18
|
+
* @param u - The value to check
|
|
19
|
+
* @returns `true` if `u` is a primitive type, `false` otherwise.
|
|
20
|
+
* When `true`, TypeScript narrows the type to `Primitive`.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* Basic usage with different value types:
|
|
24
|
+
* ```typescript
|
|
25
|
+
* isPrimitive("hello"); // true (string)
|
|
26
|
+
* isPrimitive(42); // true (number)
|
|
27
|
+
* isPrimitive(true); // true (boolean)
|
|
28
|
+
* isPrimitive(undefined); // true (undefined)
|
|
29
|
+
* isPrimitive(Symbol('test')); // true (symbol)
|
|
30
|
+
* isPrimitive(123n); // true (bigint)
|
|
31
|
+
* isPrimitive(null); // true (null is primitive despite typeof quirk)
|
|
32
|
+
*
|
|
33
|
+
* isPrimitive({}); // false (object)
|
|
34
|
+
* isPrimitive([]); // false (array)
|
|
35
|
+
* isPrimitive(() => {}); // false (function)
|
|
36
|
+
* isPrimitive(new Date()); // false (object instance)
|
|
37
|
+
* isPrimitive(/regex/); // false (RegExp object)
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* Type guard usage for separating primitives from objects:
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const values: unknown[] = [
|
|
44
|
+
* 'string',
|
|
45
|
+
* 42,
|
|
46
|
+
* true,
|
|
47
|
+
* null,
|
|
48
|
+
* undefined,
|
|
49
|
+
* {},
|
|
50
|
+
* [],
|
|
51
|
+
* new Date()
|
|
52
|
+
* ];
|
|
53
|
+
*
|
|
54
|
+
* const primitives = values.filter(isPrimitive);
|
|
55
|
+
* const objects = values.filter(value => !isPrimitive(value));
|
|
56
|
+
*
|
|
57
|
+
* primitives.forEach(primitive => {
|
|
58
|
+
* // primitive is now typed as Primitive
|
|
59
|
+
* console.log('Primitive value:', primitive);
|
|
60
|
+
* console.log('Type:', typeof primitive);
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* Deep cloning detection - primitives don't need cloning:
|
|
66
|
+
* ```typescript
|
|
67
|
+
* function deepClone<T>(value: T): T {
|
|
68
|
+
* if (isPrimitive(value)) {
|
|
69
|
+
* // Primitives are immutable, return as-is
|
|
70
|
+
* return value;
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* // Handle object cloning for non-primitives
|
|
74
|
+
* if (Array.isArray(value)) {
|
|
75
|
+
* return value.map(deepClone) as T;
|
|
76
|
+
* }
|
|
77
|
+
*
|
|
78
|
+
* if (isRecord(value)) {
|
|
79
|
+
* const cloned = {} as T;
|
|
80
|
+
* for (const key in value) {
|
|
81
|
+
* if (Object.hasOwn(value, key)) {
|
|
82
|
+
* cloned[key] = deepClone(value[key]);
|
|
83
|
+
* }
|
|
84
|
+
* }
|
|
85
|
+
* return cloned;
|
|
86
|
+
* }
|
|
87
|
+
*
|
|
88
|
+
* // For other object types, return as-is or implement specific cloning
|
|
89
|
+
* return value;
|
|
90
|
+
* }
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* Serialization helpers:
|
|
95
|
+
* ```typescript
|
|
96
|
+
* function canSerializeDirectly(value: unknown): boolean {
|
|
97
|
+
* if (isPrimitive(value)) {
|
|
98
|
+
* // Most primitives can be serialized directly
|
|
99
|
+
* return typeof value !== 'symbol' && typeof value !== 'bigint';
|
|
100
|
+
* }
|
|
101
|
+
* return false;
|
|
102
|
+
* }
|
|
103
|
+
*
|
|
104
|
+
* function safeStringify(value: unknown): string {
|
|
105
|
+
* if (isPrimitive(value)) {
|
|
106
|
+
* if (value === null) return 'null';
|
|
107
|
+
* if (value === undefined) return 'undefined';
|
|
108
|
+
* if (typeof value === 'symbol') return value.toString();
|
|
109
|
+
* if (typeof value === 'bigint') return value.toString() + 'n';
|
|
110
|
+
* return String(value);
|
|
111
|
+
* }
|
|
112
|
+
*
|
|
113
|
+
* return JSON.stringify(value);
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* Type narrowing in conditional logic:
|
|
119
|
+
* ```typescript
|
|
120
|
+
* function processValue(value: unknown): string {
|
|
121
|
+
* if (isPrimitive(value)) {
|
|
122
|
+
* // value is now Primitive type
|
|
123
|
+
* switch (typeof value) {
|
|
124
|
+
* case 'string':
|
|
125
|
+
* return `String: ${value}`;
|
|
126
|
+
* case 'number':
|
|
127
|
+
* return `Number: ${value}`;
|
|
128
|
+
* case 'boolean':
|
|
129
|
+
* return `Boolean: ${value}`;
|
|
130
|
+
* case 'undefined':
|
|
131
|
+
* return 'Undefined';
|
|
132
|
+
* case 'symbol':
|
|
133
|
+
* return `Symbol: ${value.description || 'unnamed'}`;
|
|
134
|
+
* case 'bigint':
|
|
135
|
+
* return `BigInt: ${value}n`;
|
|
136
|
+
* case 'object': // This is null
|
|
137
|
+
* return 'Null';
|
|
138
|
+
* default:
|
|
139
|
+
* return 'Unknown primitive';
|
|
140
|
+
* }
|
|
141
|
+
* } else {
|
|
142
|
+
* return `Object: ${value?.constructor?.name || 'Unknown'}`;
|
|
143
|
+
* }
|
|
144
|
+
* }
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export const isPrimitive = (u: unknown): u is Primitive => {
|
|
148
|
+
switch (typeof u) {
|
|
149
|
+
case 'string':
|
|
150
|
+
case 'number':
|
|
151
|
+
case 'boolean':
|
|
152
|
+
case 'undefined':
|
|
153
|
+
case 'symbol':
|
|
154
|
+
case 'bigint':
|
|
155
|
+
return true;
|
|
156
|
+
case 'function':
|
|
157
|
+
case 'object':
|
|
158
|
+
return u === null;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
expectType<
|
|
163
|
+
bigint | boolean | number | string | symbol | undefined | null,
|
|
164
|
+
Primitive
|
|
165
|
+
>('=');
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { isPrimitive } from './is-primitive.mjs';
|
|
3
|
+
|
|
4
|
+
describe('isPrimitive', () => {
|
|
5
|
+
test('should return true for string primitives', () => {
|
|
6
|
+
expect(isPrimitive('hello')).toBe(true);
|
|
7
|
+
expect(isPrimitive('')).toBe(true);
|
|
8
|
+
|
|
9
|
+
const value: unknown = 'test';
|
|
10
|
+
if (isPrimitive(value)) {
|
|
11
|
+
expectType<
|
|
12
|
+
typeof value,
|
|
13
|
+
bigint | boolean | number | string | symbol | undefined | null
|
|
14
|
+
>('=');
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should return true for number primitives', () => {
|
|
19
|
+
expect(isPrimitive(42)).toBe(true);
|
|
20
|
+
expect(isPrimitive(0)).toBe(true);
|
|
21
|
+
expect(isPrimitive(-3.14)).toBe(true);
|
|
22
|
+
expect(isPrimitive(Number.NaN)).toBe(true);
|
|
23
|
+
expect(isPrimitive(Number.POSITIVE_INFINITY)).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should return true for boolean primitives', () => {
|
|
27
|
+
expect(isPrimitive(true)).toBe(true);
|
|
28
|
+
expect(isPrimitive(false)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('should return true for symbol primitives', () => {
|
|
32
|
+
expect(isPrimitive(Symbol('test'))).toBe(true);
|
|
33
|
+
expect(isPrimitive(Symbol.iterator)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should return true for bigint primitives', () => {
|
|
37
|
+
expect(isPrimitive(BigInt(123))).toBe(true);
|
|
38
|
+
expect(isPrimitive(0n)).toBe(true);
|
|
39
|
+
expect(isPrimitive(-123n)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should return true for null', () => {
|
|
43
|
+
// Note: null is considered an object by typeof, so isPrimitive returns false
|
|
44
|
+
expect(isPrimitive(null)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('should return true for undefined', () => {
|
|
48
|
+
expect(isPrimitive(undefined)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should return false for objects', () => {
|
|
52
|
+
expect(isPrimitive({})).toBe(false);
|
|
53
|
+
expect(isPrimitive({ a: 1 })).toBe(false);
|
|
54
|
+
expect(isPrimitive(new Date())).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should return false for arrays', () => {
|
|
58
|
+
expect(isPrimitive([])).toBe(false);
|
|
59
|
+
expect(isPrimitive([1, 2, 3])).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should return false for functions', () => {
|
|
63
|
+
expect(isPrimitive(() => {})).toBe(false);
|
|
64
|
+
expect(isPrimitive(() => {})).toBe(false);
|
|
65
|
+
expect(isPrimitive(async () => {})).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should return false for boxed primitives', () => {
|
|
69
|
+
// eslint-disable-next-line no-new-wrappers
|
|
70
|
+
expect(isPrimitive(new String('hello'))).toBe(false);
|
|
71
|
+
// eslint-disable-next-line no-new-wrappers
|
|
72
|
+
expect(isPrimitive(new Number(42))).toBe(false);
|
|
73
|
+
// eslint-disable-next-line no-new-wrappers
|
|
74
|
+
expect(isPrimitive(new Boolean(true))).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should narrow types correctly in conditional', () => {
|
|
78
|
+
const values: unknown[] = [
|
|
79
|
+
'string',
|
|
80
|
+
42,
|
|
81
|
+
true,
|
|
82
|
+
{},
|
|
83
|
+
[],
|
|
84
|
+
null,
|
|
85
|
+
undefined,
|
|
86
|
+
Symbol('test'),
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const primitives = values.filter(isPrimitive);
|
|
90
|
+
const nonPrimitives = values.filter((v) => !isPrimitive(v));
|
|
91
|
+
|
|
92
|
+
expect(primitives.length).toBe(6); // string, 42, true, null, undefined, symbol
|
|
93
|
+
expect(primitives[0]).toBe('string');
|
|
94
|
+
expect(primitives[1]).toBe(42);
|
|
95
|
+
expect(primitives[2]).toBe(true);
|
|
96
|
+
expect(primitives[3]).toBe(null);
|
|
97
|
+
expect(primitives[4]).toBe(undefined);
|
|
98
|
+
expect(typeof primitives[5]).toBe('symbol');
|
|
99
|
+
|
|
100
|
+
expect(nonPrimitives).toStrictEqual([{}, []]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isNonNullObject } from './is-non-null-object.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type guard that checks if a value is a plain object (record) - a non-null object that is not an array.
|
|
5
|
+
*
|
|
6
|
+
* This function is useful for identifying "plain" JavaScript objects (also called records or
|
|
7
|
+
* dictionaries) - objects that are typically used as key-value collections. It excludes arrays,
|
|
8
|
+
* functions, and special object types like Date, RegExp, etc., focusing on objects that can be
|
|
9
|
+
* safely treated as property collections.
|
|
10
|
+
*
|
|
11
|
+
* **Type Narrowing Behavior:**
|
|
12
|
+
* - Narrows `unknown` to `UnknownRecord` (equivalent to `Record<PropertyKey, unknown>`)
|
|
13
|
+
* - Excludes `null`, `undefined`, primitives, arrays, and functions
|
|
14
|
+
* - Returns `true` for plain objects `{}`, object literals, and objects created with `Object.create()`
|
|
15
|
+
* - Returns `false` for arrays, even though they are technically objects
|
|
16
|
+
*
|
|
17
|
+
* **Implementation:** Uses `isNonNullObject()` to check for objects, then `Array.isArray()` to exclude arrays.
|
|
18
|
+
*
|
|
19
|
+
* @param u - The value to check
|
|
20
|
+
* @returns `true` if `u` is a non-null object and not an array, `false` otherwise.
|
|
21
|
+
* When `true`, TypeScript narrows the type to `UnknownRecord`.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* Basic usage with different value types:
|
|
25
|
+
* ```typescript
|
|
26
|
+
* isRecord({}); // true (empty object)
|
|
27
|
+
* isRecord({ name: 'John' }); // true (object literal)
|
|
28
|
+
* isRecord(Object.create(null)); // true (object created with Object.create)
|
|
29
|
+
* isRecord(new Object()); // true (object constructor)
|
|
30
|
+
*
|
|
31
|
+
* isRecord([]); // false (array)
|
|
32
|
+
* isRecord([1, 2, 3]); // false (array with elements)
|
|
33
|
+
* isRecord(null); // false (null)
|
|
34
|
+
* isRecord(undefined); // false (undefined)
|
|
35
|
+
* isRecord("string"); // false (primitive)
|
|
36
|
+
* isRecord(42); // false (primitive)
|
|
37
|
+
* isRecord(() => {}); // false (function)
|
|
38
|
+
* isRecord(new Date()); // false (Date object - not a plain record)
|
|
39
|
+
* isRecord(/regex/); // false (RegExp object)
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* Type guard usage for safe property access:
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const apiResponse: unknown = await fetchUserData();
|
|
46
|
+
*
|
|
47
|
+
* if (isRecord(apiResponse)) {
|
|
48
|
+
* // apiResponse is now typed as UnknownRecord
|
|
49
|
+
* console.log('Response keys:', Object.keys(apiResponse));
|
|
50
|
+
*
|
|
51
|
+
* // Safe to access properties (though values are still unknown)
|
|
52
|
+
* const userId = apiResponse.id; // Type: unknown
|
|
53
|
+
* const userName = apiResponse.name; // Type: unknown
|
|
54
|
+
*
|
|
55
|
+
* // You can combine with other type guards for further narrowing
|
|
56
|
+
* if (hasKey(apiResponse, 'id') && isString(apiResponse.id)) {
|
|
57
|
+
* console.log('User ID:', apiResponse.id); // Now safely typed as string
|
|
58
|
+
* }
|
|
59
|
+
* } else {
|
|
60
|
+
* console.log('API response is not a valid object');
|
|
61
|
+
* }
|
|
62
|
+
* ```
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* Filtering mixed arrays to find plain objects:
|
|
66
|
+
* ```typescript
|
|
67
|
+
* const mixedData: unknown[] = [
|
|
68
|
+
* { type: 'user', name: 'Alice' },
|
|
69
|
+
* [1, 2, 3],
|
|
70
|
+
* 'string',
|
|
71
|
+
* { type: 'admin', permissions: ['read', 'write'] },
|
|
72
|
+
* new Date(),
|
|
73
|
+
* null,
|
|
74
|
+
* { id: 123 }
|
|
75
|
+
* ];
|
|
76
|
+
*
|
|
77
|
+
* const records = mixedData.filter(isRecord);
|
|
78
|
+
* // records contains only the plain objects:
|
|
79
|
+
* // [{ type: 'user', name: 'Alice' }, { type: 'admin', permissions: [...] }, { id: 123 }]
|
|
80
|
+
*
|
|
81
|
+
* records.forEach(record => {
|
|
82
|
+
* // Each record is guaranteed to be UnknownRecord
|
|
83
|
+
* const keys = Object.keys(record);
|
|
84
|
+
* console.log('Object keys:', keys);
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* Progressive validation of nested structures:
|
|
90
|
+
* ```typescript
|
|
91
|
+
* interface User {
|
|
92
|
+
* id: string;
|
|
93
|
+
* profile: {
|
|
94
|
+
* name: string;
|
|
95
|
+
* email: string;
|
|
96
|
+
* };
|
|
97
|
+
* }
|
|
98
|
+
*
|
|
99
|
+
* function validateUser(data: unknown): User | null {
|
|
100
|
+
* if (!isRecord(data)) {
|
|
101
|
+
* return null;
|
|
102
|
+
* }
|
|
103
|
+
*
|
|
104
|
+
* // data is now UnknownRecord
|
|
105
|
+
* if (!hasKey(data, 'id') || !isString(data.id)) {
|
|
106
|
+
* return null;
|
|
107
|
+
* }
|
|
108
|
+
*
|
|
109
|
+
* if (!hasKey(data, 'profile') || !isRecord(data.profile)) {
|
|
110
|
+
* return null;
|
|
111
|
+
* }
|
|
112
|
+
*
|
|
113
|
+
* const profile = data.profile;
|
|
114
|
+
* if (!hasKey(profile, 'name') || !isString(profile.name) ||
|
|
115
|
+
* !hasKey(profile, 'email') || !isString(profile.email)) {
|
|
116
|
+
* return null;
|
|
117
|
+
* }
|
|
118
|
+
*
|
|
119
|
+
* return {
|
|
120
|
+
* id: data.id,
|
|
121
|
+
* profile: {
|
|
122
|
+
* name: profile.name,
|
|
123
|
+
* email: profile.email
|
|
124
|
+
* }
|
|
125
|
+
* };
|
|
126
|
+
* }
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* Object transformation and mapping:
|
|
131
|
+
* ```typescript
|
|
132
|
+
* function transformRecords(data: unknown[]): Record<string, unknown>[] {
|
|
133
|
+
* return data
|
|
134
|
+
* .filter(isRecord) // Keep only plain objects
|
|
135
|
+
* .map(record => {
|
|
136
|
+
* // Transform each record
|
|
137
|
+
* const transformed: Record<string, unknown> = {};
|
|
138
|
+
*
|
|
139
|
+
* for (const [key, value] of Object.entries(record)) {
|
|
140
|
+
* // Apply some transformation logic
|
|
141
|
+
* transformed[key.toLowerCase()] = value;
|
|
142
|
+
* }
|
|
143
|
+
*
|
|
144
|
+
* return transformed;
|
|
145
|
+
* });
|
|
146
|
+
* }
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* @see {@link isNonNullObject} - For checking any object type (includes arrays)
|
|
150
|
+
* @see {@link hasKey} - For checking if a record has specific keys
|
|
151
|
+
*/
|
|
152
|
+
export const isRecord = (u: unknown): u is UnknownRecord =>
|
|
153
|
+
isNonNullObject(u) && !Array.isArray(u);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { isRecord } from './is-record.mjs';
|
|
3
|
+
|
|
4
|
+
describe('isRecord', () => {
|
|
5
|
+
test('{ x: 1 } is a record', () => {
|
|
6
|
+
const obj = { x: 1 } as const;
|
|
7
|
+
const unk: unknown = obj;
|
|
8
|
+
const res = isRecord(unk);
|
|
9
|
+
|
|
10
|
+
expectType<typeof obj, UnknownRecord>('<=');
|
|
11
|
+
expectType<typeof res, boolean>('=');
|
|
12
|
+
|
|
13
|
+
if (res) {
|
|
14
|
+
expectType<typeof unk, UnknownRecord>('=');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
expect(res).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('{} is a record', () => {
|
|
21
|
+
const obj = {} as const;
|
|
22
|
+
const unk: unknown = obj;
|
|
23
|
+
const res = isRecord(unk);
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
26
|
+
expectType<typeof obj, {}>('=');
|
|
27
|
+
expectType<typeof res, boolean>('=');
|
|
28
|
+
|
|
29
|
+
if (res) {
|
|
30
|
+
expectType<typeof unk, UnknownRecord>('=');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
expect(res).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('[] is not a record', () => {
|
|
37
|
+
const obj: DeepReadonly<never[]> = [] as const;
|
|
38
|
+
const unk: unknown = obj;
|
|
39
|
+
const res = isRecord(unk);
|
|
40
|
+
|
|
41
|
+
expectType<typeof obj, readonly never[]>('=');
|
|
42
|
+
expectType<typeof res, boolean>('=');
|
|
43
|
+
|
|
44
|
+
expect(res).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('null is not a record', () => {
|
|
48
|
+
const obj = null;
|
|
49
|
+
const unk: unknown = obj;
|
|
50
|
+
const res = isRecord(unk);
|
|
51
|
+
|
|
52
|
+
expectType<typeof obj, null>('=');
|
|
53
|
+
expectType<typeof res, boolean>('=');
|
|
54
|
+
|
|
55
|
+
expect(res).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('undefined is not a record', () => {
|
|
59
|
+
const obj = undefined;
|
|
60
|
+
const unk: unknown = obj;
|
|
61
|
+
const res = isRecord(unk);
|
|
62
|
+
|
|
63
|
+
expectType<typeof obj, undefined>('=');
|
|
64
|
+
expectType<typeof res, boolean>('=');
|
|
65
|
+
|
|
66
|
+
expect(res).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('3 is not a record', () => {
|
|
70
|
+
const obj = 3;
|
|
71
|
+
const unk: unknown = obj;
|
|
72
|
+
const res = isRecord(unk);
|
|
73
|
+
|
|
74
|
+
expectType<typeof obj, 3>('=');
|
|
75
|
+
expectType<typeof res, boolean>('=');
|
|
76
|
+
|
|
77
|
+
expect(res).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('"str" is not a record', () => {
|
|
81
|
+
const obj = 'str';
|
|
82
|
+
const unk: unknown = obj;
|
|
83
|
+
const res = isRecord(unk);
|
|
84
|
+
|
|
85
|
+
expectType<typeof obj, 'str'>('=');
|
|
86
|
+
expectType<typeof res, boolean>('=');
|
|
87
|
+
|
|
88
|
+
expect(res).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// test('Map is not a record', () => {
|
|
92
|
+
// const obj = new MutableMap();
|
|
93
|
+
// const unk: unknown = obj;
|
|
94
|
+
// const res = isRecord(unk);
|
|
95
|
+
|
|
96
|
+
// assertNotType<typeof obj, UnknownRecord>("<=");
|
|
97
|
+
// expectType<typeof res, boolean>("=");
|
|
98
|
+
|
|
99
|
+
// expect(res).toBe(false);
|
|
100
|
+
// });
|
|
101
|
+
|
|
102
|
+
// test('Set is not a record', () => {
|
|
103
|
+
// const obj = new MutableSet();
|
|
104
|
+
// const unk: unknown = obj;
|
|
105
|
+
// const res = isRecord(unk);
|
|
106
|
+
|
|
107
|
+
// assertNotType<typeof obj, UnknownRecord>("<=");
|
|
108
|
+
// expectType<typeof res, boolean>("=");
|
|
109
|
+
|
|
110
|
+
// expect(res).toBe(false);
|
|
111
|
+
// });
|
|
112
|
+
});
|