ts-data-forge 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +534 -0
  3. package/package.json +101 -0
  4. package/src/array/array-utils-creation.test.mts +443 -0
  5. package/src/array/array-utils-modification.test.mts +197 -0
  6. package/src/array/array-utils-overload-type-error.test.mts +149 -0
  7. package/src/array/array-utils-reducing-value.test.mts +425 -0
  8. package/src/array/array-utils-search.test.mts +169 -0
  9. package/src/array/array-utils-set-op.test.mts +335 -0
  10. package/src/array/array-utils-slice-clamped.test.mts +113 -0
  11. package/src/array/array-utils-slicing.test.mts +316 -0
  12. package/src/array/array-utils-transformation.test.mts +790 -0
  13. package/src/array/array-utils-validation.test.mts +492 -0
  14. package/src/array/array-utils.mts +4000 -0
  15. package/src/array/array.test.mts +146 -0
  16. package/src/array/index.mts +2 -0
  17. package/src/array/tuple-utils.mts +519 -0
  18. package/src/array/tuple-utils.test.mts +518 -0
  19. package/src/collections/imap-mapped.mts +801 -0
  20. package/src/collections/imap-mapped.test.mts +860 -0
  21. package/src/collections/imap.mts +651 -0
  22. package/src/collections/imap.test.mts +932 -0
  23. package/src/collections/index.mts +6 -0
  24. package/src/collections/iset-mapped.mts +889 -0
  25. package/src/collections/iset-mapped.test.mts +1187 -0
  26. package/src/collections/iset.mts +682 -0
  27. package/src/collections/iset.test.mts +1084 -0
  28. package/src/collections/queue.mts +390 -0
  29. package/src/collections/queue.test.mts +282 -0
  30. package/src/collections/stack.mts +423 -0
  31. package/src/collections/stack.test.mts +225 -0
  32. package/src/expect-type.mts +206 -0
  33. package/src/functional/index.mts +4 -0
  34. package/src/functional/match.mts +300 -0
  35. package/src/functional/match.test.mts +177 -0
  36. package/src/functional/optional.mts +733 -0
  37. package/src/functional/optional.test.mts +619 -0
  38. package/src/functional/pipe.mts +212 -0
  39. package/src/functional/pipe.test.mts +85 -0
  40. package/src/functional/result.mts +1134 -0
  41. package/src/functional/result.test.mts +777 -0
  42. package/src/globals.d.mts +38 -0
  43. package/src/guard/has-key.mts +119 -0
  44. package/src/guard/has-key.test.mts +219 -0
  45. package/src/guard/index.mts +7 -0
  46. package/src/guard/is-non-empty-string.mts +108 -0
  47. package/src/guard/is-non-empty-string.test.mts +91 -0
  48. package/src/guard/is-non-null-object.mts +106 -0
  49. package/src/guard/is-non-null-object.test.mts +90 -0
  50. package/src/guard/is-primitive.mts +165 -0
  51. package/src/guard/is-primitive.test.mts +102 -0
  52. package/src/guard/is-record.mts +153 -0
  53. package/src/guard/is-record.test.mts +112 -0
  54. package/src/guard/is-type.mts +450 -0
  55. package/src/guard/is-type.test.mts +496 -0
  56. package/src/guard/key-is-in.mts +163 -0
  57. package/src/guard/key-is-in.test.mts +19 -0
  58. package/src/index.mts +10 -0
  59. package/src/iterator/index.mts +1 -0
  60. package/src/iterator/range.mts +120 -0
  61. package/src/iterator/range.test.mts +33 -0
  62. package/src/json/index.mts +1 -0
  63. package/src/json/json.mts +711 -0
  64. package/src/json/json.test.mts +628 -0
  65. package/src/number/branded-types/finite-number.mts +354 -0
  66. package/src/number/branded-types/finite-number.test.mts +135 -0
  67. package/src/number/branded-types/index.mts +26 -0
  68. package/src/number/branded-types/int.mts +278 -0
  69. package/src/number/branded-types/int.test.mts +140 -0
  70. package/src/number/branded-types/int16.mts +192 -0
  71. package/src/number/branded-types/int16.test.mts +170 -0
  72. package/src/number/branded-types/int32.mts +193 -0
  73. package/src/number/branded-types/int32.test.mts +170 -0
  74. package/src/number/branded-types/non-negative-finite-number.mts +223 -0
  75. package/src/number/branded-types/non-negative-finite-number.test.mts +188 -0
  76. package/src/number/branded-types/non-negative-int16.mts +187 -0
  77. package/src/number/branded-types/non-negative-int16.test.mts +201 -0
  78. package/src/number/branded-types/non-negative-int32.mts +187 -0
  79. package/src/number/branded-types/non-negative-int32.test.mts +204 -0
  80. package/src/number/branded-types/non-zero-finite-number.mts +229 -0
  81. package/src/number/branded-types/non-zero-finite-number.test.mts +198 -0
  82. package/src/number/branded-types/non-zero-int.mts +167 -0
  83. package/src/number/branded-types/non-zero-int.test.mts +177 -0
  84. package/src/number/branded-types/non-zero-int16.mts +196 -0
  85. package/src/number/branded-types/non-zero-int16.test.mts +195 -0
  86. package/src/number/branded-types/non-zero-int32.mts +196 -0
  87. package/src/number/branded-types/non-zero-int32.test.mts +197 -0
  88. package/src/number/branded-types/non-zero-safe-int.mts +196 -0
  89. package/src/number/branded-types/non-zero-safe-int.test.mts +232 -0
  90. package/src/number/branded-types/non-zero-uint16.mts +189 -0
  91. package/src/number/branded-types/non-zero-uint16.test.mts +199 -0
  92. package/src/number/branded-types/non-zero-uint32.mts +189 -0
  93. package/src/number/branded-types/non-zero-uint32.test.mts +199 -0
  94. package/src/number/branded-types/positive-finite-number.mts +241 -0
  95. package/src/number/branded-types/positive-finite-number.test.mts +204 -0
  96. package/src/number/branded-types/positive-int.mts +304 -0
  97. package/src/number/branded-types/positive-int.test.mts +176 -0
  98. package/src/number/branded-types/positive-int16.mts +188 -0
  99. package/src/number/branded-types/positive-int16.test.mts +197 -0
  100. package/src/number/branded-types/positive-int32.mts +188 -0
  101. package/src/number/branded-types/positive-int32.test.mts +197 -0
  102. package/src/number/branded-types/positive-safe-int.mts +187 -0
  103. package/src/number/branded-types/positive-safe-int.test.mts +210 -0
  104. package/src/number/branded-types/positive-uint16.mts +188 -0
  105. package/src/number/branded-types/positive-uint16.test.mts +203 -0
  106. package/src/number/branded-types/positive-uint32.mts +188 -0
  107. package/src/number/branded-types/positive-uint32.test.mts +203 -0
  108. package/src/number/branded-types/safe-int.mts +291 -0
  109. package/src/number/branded-types/safe-int.test.mts +170 -0
  110. package/src/number/branded-types/safe-uint.mts +187 -0
  111. package/src/number/branded-types/safe-uint.test.mts +176 -0
  112. package/src/number/branded-types/uint.mts +179 -0
  113. package/src/number/branded-types/uint.test.mts +158 -0
  114. package/src/number/branded-types/uint16.mts +186 -0
  115. package/src/number/branded-types/uint16.test.mts +170 -0
  116. package/src/number/branded-types/uint32.mts +218 -0
  117. package/src/number/branded-types/uint32.test.mts +170 -0
  118. package/src/number/enum/index.mts +2 -0
  119. package/src/number/enum/int8.mts +344 -0
  120. package/src/number/enum/int8.test.mts +180 -0
  121. package/src/number/enum/uint8.mts +293 -0
  122. package/src/number/enum/uint8.test.mts +164 -0
  123. package/src/number/index.mts +4 -0
  124. package/src/number/num.mts +604 -0
  125. package/src/number/num.test.mts +242 -0
  126. package/src/number/refined-number-utils.mts +566 -0
  127. package/src/object/index.mts +1 -0
  128. package/src/object/object.mts +447 -0
  129. package/src/object/object.test.mts +124 -0
  130. package/src/others/cast-mutable.mts +113 -0
  131. package/src/others/cast-readonly.mts +192 -0
  132. package/src/others/cast-readonly.test.mts +89 -0
  133. package/src/others/if-then.mts +98 -0
  134. package/src/others/if-then.test.mts +75 -0
  135. package/src/others/index.mts +7 -0
  136. package/src/others/map-nullable.mts +172 -0
  137. package/src/others/map-nullable.test.mts +297 -0
  138. package/src/others/memoize-function.mts +196 -0
  139. package/src/others/memoize-function.test.mts +168 -0
  140. package/src/others/tuple.mts +160 -0
  141. package/src/others/tuple.test.mts +11 -0
  142. package/src/others/unknown-to-string.mts +215 -0
  143. package/src/others/unknown-to-string.test.mts +114 -0
@@ -0,0 +1,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
+ });