ts-data-forge 7.1.1 → 9.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 (61) hide show
  1. package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
  2. package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
  3. package/dist/entry-point.mjs +1 -0
  4. package/dist/entry-point.mjs.map +1 -1
  5. package/dist/expect-type.d.mts.map +1 -1
  6. package/dist/expect-type.mjs.map +1 -1
  7. package/dist/functional/ternary-result/ternary-result.d.mts.map +1 -1
  8. package/dist/guard/index.d.mts +1 -0
  9. package/dist/guard/index.d.mts.map +1 -1
  10. package/dist/guard/index.mjs +1 -0
  11. package/dist/guard/index.mjs.map +1 -1
  12. package/dist/guard/is-length-bounded-string.d.mts +164 -0
  13. package/dist/guard/is-length-bounded-string.d.mts.map +1 -0
  14. package/dist/guard/is-length-bounded-string.mjs +164 -0
  15. package/dist/guard/is-length-bounded-string.mjs.map +1 -0
  16. package/dist/guard/is-non-null-object.d.mts +3 -3
  17. package/dist/guard/is-non-null-object.mjs +3 -3
  18. package/dist/guard/is-record.d.mts +47 -29
  19. package/dist/guard/is-record.d.mts.map +1 -1
  20. package/dist/guard/is-record.mjs +48 -30
  21. package/dist/guard/is-record.mjs.map +1 -1
  22. package/dist/index.mjs +1 -0
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/number/branded-types/int.d.mts +1 -1
  25. package/dist/number/branded-types/int16.d.mts +1 -1
  26. package/dist/number/branded-types/int32.d.mts +1 -1
  27. package/dist/number/branded-types/non-negative-int16.d.mts +1 -1
  28. package/dist/number/branded-types/non-negative-int32.d.mts +1 -1
  29. package/dist/number/branded-types/positive-int.d.mts +1 -1
  30. package/dist/number/branded-types/positive-int16.d.mts +1 -1
  31. package/dist/number/branded-types/positive-int32.d.mts +1 -1
  32. package/dist/number/branded-types/positive-safe-int.d.mts +1 -1
  33. package/dist/number/branded-types/positive-uint16.d.mts +1 -1
  34. package/dist/number/branded-types/positive-uint32.d.mts +1 -1
  35. package/dist/number/branded-types/safe-int.d.mts +1 -1
  36. package/dist/number/branded-types/safe-uint.d.mts +1 -1
  37. package/dist/number/branded-types/uint.d.mts +1 -1
  38. package/dist/number/branded-types/uint16.d.mts +1 -1
  39. package/dist/number/branded-types/uint32.d.mts +1 -1
  40. package/dist/number/refined-number-utils.d.mts.map +1 -1
  41. package/dist/number/refined-number-utils.mjs.map +1 -1
  42. package/dist/object/object.d.mts.map +1 -1
  43. package/dist/object/object.mjs.map +1 -1
  44. package/dist/others/fast-deep-equal.d.mts.map +1 -1
  45. package/dist/others/fast-deep-equal.mjs +7 -5
  46. package/dist/others/fast-deep-equal.mjs.map +1 -1
  47. package/package.json +12 -12
  48. package/src/array/impl/array-utils-transformation.mts +1 -2
  49. package/src/array/impl/array-utils-transformation.test.mts +3 -36
  50. package/src/array/impl/array-utils-validation.test.mts +1 -3
  51. package/src/expect-type.mts +4 -5
  52. package/src/functional/ternary-result/ternary-result.mts +2 -6
  53. package/src/guard/index.mts +1 -0
  54. package/src/guard/is-length-bounded-string.mts +199 -0
  55. package/src/guard/is-length-bounded-string.test.mts +170 -0
  56. package/src/guard/is-non-null-object.mts +3 -3
  57. package/src/guard/is-record.mts +49 -30
  58. package/src/guard/is-record.test.mts +133 -61
  59. package/src/number/refined-number-utils.mts +1 -9
  60. package/src/object/object.mts +25 -23
  61. package/src/others/fast-deep-equal.mts +10 -8
@@ -645,18 +645,7 @@ describe('Arr transformations', () => {
645
645
  expectType<
646
646
  typeof result,
647
647
  readonly (readonly (
648
- | 1
649
- | 2
650
- | 3
651
- | 4
652
- | 5
653
- | 6
654
- | 7
655
- | 8
656
- | 9
657
- | 10
658
- | 11
659
- | 12
648
+ 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
660
649
  )[])[]
661
650
  >('=');
662
651
 
@@ -675,18 +664,7 @@ describe('Arr transformations', () => {
675
664
  expectType<
676
665
  typeof result,
677
666
  readonly (readonly (
678
- | 1
679
- | 2
680
- | 3
681
- | 4
682
- | 5
683
- | 6
684
- | 7
685
- | 8
686
- | 9
687
- | 10
688
- | 11
689
- | 12
667
+ 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
690
668
  )[])[]
691
669
  >('=');
692
670
 
@@ -706,18 +684,7 @@ describe('Arr transformations', () => {
706
684
  expectType<
707
685
  typeof result,
708
686
  readonly (readonly (
709
- | 1
710
- | 2
711
- | 3
712
- | 4
713
- | 5
714
- | 6
715
- | 7
716
- | 8
717
- | 9
718
- | 10
719
- | 11
720
- | 12
687
+ 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12
721
688
  )[])[]
722
689
  >('=');
723
690
 
@@ -228,9 +228,7 @@ describe('Arr validations', () => {
228
228
 
229
229
  test('should handle union of array types', () => {
230
230
  type MixedArrayUnion =
231
- | readonly string[]
232
- | readonly number[]
233
- | readonly boolean[];
231
+ readonly string[] | readonly number[] | readonly boolean[];
234
232
 
235
233
  const mixedArray: MixedArrayUnion = [1, 2, 3] as const;
236
234
 
@@ -71,9 +71,8 @@ import { type TypeEq, type TypeExtends } from 'ts-type-forge';
71
71
  export const expectType = <A, B>(
72
72
  _relation: TypeEq<A, B> extends true
73
73
  ? '<=' | '=' | '>=' | '~='
74
- :
75
- | '!='
76
- | (TypeExtends<A, B> extends true
77
- ? '<=' | (TypeExtends<B, A> extends true ? '>=' | '~=' : '!>=')
78
- : '!<=' | (TypeExtends<B, A> extends true ? '>=' : '!>=')),
74
+ : | '!='
75
+ | (TypeExtends<A, B> extends true
76
+ ? '<=' | (TypeExtends<B, A> extends true ? '>=' | '~=' : '!>=')
77
+ : '!<=' | (TypeExtends<B, A> extends true ? '>=' : '!>=')),
79
78
  ): void => undefined;
@@ -39,16 +39,12 @@ export type TernaryErr<E> = Readonly<{
39
39
  * Represents a value that can be `Ok`, `Warn`, or `Err`.
40
40
  */
41
41
  export type TernaryResult<S, E, W = E> =
42
- | TernaryOk<S>
43
- | TernaryWarn<S, W>
44
- | TernaryErr<E>;
42
+ TernaryOk<S> | TernaryWarn<S, W> | TernaryErr<E>;
45
43
 
46
44
  /**
47
45
  * Base type for any `TernaryResult`, used for generic constraints.
48
46
  */
49
47
  export type UnknownTernaryResult =
50
- | TernaryOk<unknown>
51
- | TernaryWarn<unknown, unknown>
52
- | TernaryErr<unknown>;
48
+ TernaryOk<unknown> | TernaryWarn<unknown, unknown> | TernaryErr<unknown>;
53
49
 
54
50
  export * as TernaryResult from './impl/index.mjs';
@@ -1,4 +1,5 @@
1
1
  export * from './has-key.mjs';
2
+ export * from './is-length-bounded-string.mjs';
2
3
  export * from './is-non-empty-string.mjs';
3
4
  export * from './is-non-null-object.mjs';
4
5
  export * from './is-primitive.mjs';
@@ -0,0 +1,199 @@
1
+ import {
2
+ type BoundedLengthString,
3
+ type FixedLengthString,
4
+ type MaxLengthString,
5
+ type MinLengthString,
6
+ } from 'ts-type-forge';
7
+ import { type SizeType } from '../types.mjs';
8
+
9
+ /**
10
+ * Type guard that checks if a string has at least `minLength` characters.
11
+ *
12
+ * The length is measured in UTF-16 code units (the same unit as
13
+ * `String.prototype.length`), so characters outside the Basic Multilingual
14
+ * Plane (e.g. emoji) count as 2.
15
+ *
16
+ * **Type Narrowing Behavior:**
17
+ *
18
+ * - Narrows the input to `MinLengthString<MinLength>` while preserving the
19
+ * original string type (e.g. literal types) via intersection.
20
+ * - The result participates in the length-constraint subtyping relation: for
21
+ * example, a value narrowed to `MinLengthString<3>` is assignable to
22
+ * `MinLengthString<1>` (= `NonEmptyString`).
23
+ *
24
+ * @example
25
+ *
26
+ * ```ts
27
+ * const input: string = 'very-secret-password';
28
+ *
29
+ * assert.isTrue(isMinLengthString(input, 12));
30
+ *
31
+ * assert.isFalse(isMinLengthString('short', 12));
32
+ *
33
+ * if (isMinLengthString(input, 12)) {
34
+ * const password: MinLengthString<12> = input;
35
+ *
36
+ * const nonEmpty: NonEmptyString = input; // OK (12 >= 1)
37
+ * }
38
+ * ```
39
+ *
40
+ * @template S - The input string type (literal types are preserved).
41
+ * @template MinLength - The minimum number of characters (inclusive).
42
+ * @param s - The string to check.
43
+ * @param minLength - The minimum number of characters (inclusive).
44
+ * @returns `true` if `s.length >= minLength`, `false` otherwise. When `true`,
45
+ * TypeScript narrows `s` to `MinLengthString<MinLength> & S`.
46
+ */
47
+ export const isMinLengthString = <
48
+ S extends string,
49
+ MinLength extends SizeType.ArgStr,
50
+ >(
51
+ s: S,
52
+ minLength: MinLength,
53
+ ): s is MinLengthString<MinLength> & S => s.length >= minLength;
54
+
55
+ /**
56
+ * Type guard that checks if a string has at most `maxLength` characters.
57
+ *
58
+ * The length is measured in UTF-16 code units (the same unit as
59
+ * `String.prototype.length`), so characters outside the Basic Multilingual
60
+ * Plane (e.g. emoji) count as 2.
61
+ *
62
+ * **Type Narrowing Behavior:**
63
+ *
64
+ * - Narrows the input to `MaxLengthString<MaxLength>` while preserving the
65
+ * original string type (e.g. literal types) via intersection.
66
+ * - The result participates in the length-constraint subtyping relation: for
67
+ * example, a value narrowed to `MaxLengthString<16>` is assignable to
68
+ * `MaxLengthString<32>`.
69
+ *
70
+ * @example
71
+ *
72
+ * ```ts
73
+ * const input: string = 'noshiro';
74
+ *
75
+ * assert.isTrue(isMaxLengthString(input, 32));
76
+ *
77
+ * assert.isFalse(isMaxLengthString(input, 3));
78
+ *
79
+ * if (isMaxLengthString(input, 32)) {
80
+ * const userName: MaxLengthString<32> = input;
81
+ *
82
+ * const relaxed: MaxLengthString<64> = input; // OK (32 <= 64)
83
+ * }
84
+ * ```
85
+ *
86
+ * @template S - The input string type (literal types are preserved).
87
+ * @template MaxLength - The maximum number of characters (inclusive).
88
+ * @param s - The string to check.
89
+ * @param maxLength - The maximum number of characters (inclusive).
90
+ * @returns `true` if `s.length <= maxLength`, `false` otherwise. When `true`,
91
+ * TypeScript narrows `s` to `MaxLengthString<MaxLength> & S`.
92
+ */
93
+ export const isMaxLengthString = <
94
+ S extends string,
95
+ MaxLength extends SizeType.ArgStr,
96
+ >(
97
+ s: S,
98
+ maxLength: MaxLength,
99
+ ): s is MaxLengthString<MaxLength> & S => s.length <= maxLength;
100
+
101
+ /**
102
+ * Type guard that checks if a string's length is within the inclusive range
103
+ * `[minLength, maxLength]`.
104
+ *
105
+ * The length is measured in UTF-16 code units (the same unit as
106
+ * `String.prototype.length`), so characters outside the Basic Multilingual
107
+ * Plane (e.g. emoji) count as 2.
108
+ *
109
+ * **Type Narrowing Behavior:**
110
+ *
111
+ * - Narrows the input to `BoundedLengthString<MinLength, MaxLength>` while
112
+ * preserving the original string type (e.g. literal types) via intersection.
113
+ * - Since `BoundedLengthString` is the intersection of `MinLengthString` and
114
+ * `MaxLengthString`, the narrowed value is assignable to both, and both
115
+ * bounds can be weakened independently.
116
+ *
117
+ * @example
118
+ *
119
+ * ```ts
120
+ * const input: string = 'user-12345678';
121
+ *
122
+ * assert.isTrue(isBoundedLengthString(input, 8, 16));
123
+ *
124
+ * assert.isFalse(isBoundedLengthString('user', 8, 16));
125
+ *
126
+ * if (isBoundedLengthString(input, 8, 16)) {
127
+ * const userId: BoundedLengthString<8, 16> = input;
128
+ *
129
+ * const relaxed: BoundedLengthString<1, 255> = input; // OK ([8, 16] ⊆ [1, 255])
130
+ * }
131
+ * ```
132
+ *
133
+ * @template S - The input string type (literal types are preserved).
134
+ * @template MinLength - The minimum number of characters (inclusive).
135
+ * @template MaxLength - The maximum number of characters (inclusive).
136
+ * @param s - The string to check.
137
+ * @param minLength - The minimum number of characters (inclusive).
138
+ * @param maxLength - The maximum number of characters (inclusive).
139
+ * @returns `true` if `minLength <= s.length && s.length <= maxLength`, `false`
140
+ * otherwise. When `true`, TypeScript narrows `s` to
141
+ * `BoundedLengthString<MinLength, MaxLength> & S`.
142
+ */
143
+ export const isBoundedLengthString = <
144
+ S extends string,
145
+ MinLength extends SizeType.ArgStr,
146
+ MaxLength extends SizeType.ArgStr,
147
+ >(
148
+ s: S,
149
+ minLength: MinLength,
150
+ maxLength: MaxLength,
151
+ ): s is BoundedLengthString<MinLength, MaxLength> & S =>
152
+ s.length >= minLength && s.length <= maxLength;
153
+
154
+ /**
155
+ * Type guard that checks if a string has exactly `length` characters.
156
+ *
157
+ * The length is measured in UTF-16 code units (the same unit as
158
+ * `String.prototype.length`), so characters outside the Basic Multilingual
159
+ * Plane (e.g. emoji) count as 2.
160
+ *
161
+ * **Type Narrowing Behavior:**
162
+ *
163
+ * - Narrows the input to `FixedLengthString<Length>` while preserving the
164
+ * original string type (e.g. literal types) via intersection.
165
+ * - Since `FixedLengthString<Length>` is defined as
166
+ * `BoundedLengthString<Length, Length>`, the narrowed value is assignable to
167
+ * any `MinLengthString<N>` with `N <= Length` and any `MaxLengthString<N>`
168
+ * with `N >= Length`.
169
+ *
170
+ * @example
171
+ *
172
+ * ```ts
173
+ * const input: string = 'JP';
174
+ *
175
+ * assert.isTrue(isFixedLengthString(input, 2));
176
+ *
177
+ * assert.isFalse(isFixedLengthString(input, 3));
178
+ *
179
+ * if (isFixedLengthString(input, 2)) {
180
+ * const countryCode: FixedLengthString<2> = input;
181
+ *
182
+ * const atMost5: MaxLengthString<5> = input; // OK (2 <= 5)
183
+ * }
184
+ * ```
185
+ *
186
+ * @template S - The input string type (literal types are preserved).
187
+ * @template Length - The exact number of characters.
188
+ * @param s - The string to check.
189
+ * @param length - The exact number of characters.
190
+ * @returns `true` if `s.length === length`, `false` otherwise. When `true`,
191
+ * TypeScript narrows `s` to `FixedLengthString<Length> & S`.
192
+ */
193
+ export const isFixedLengthString = <
194
+ S extends string,
195
+ Length extends SizeType.ArgStr,
196
+ >(
197
+ s: S,
198
+ length: Length,
199
+ ): s is FixedLengthString<Length> & S => s.length === length;
@@ -0,0 +1,170 @@
1
+ import {
2
+ type BoundedLengthString,
3
+ type FixedLengthString,
4
+ type MaxLengthString,
5
+ type MinLengthString,
6
+ type NonEmptyString,
7
+ } from 'ts-type-forge';
8
+ import { expectType } from '../expect-type.mjs';
9
+ import {
10
+ isBoundedLengthString,
11
+ isFixedLengthString,
12
+ isMaxLengthString,
13
+ isMinLengthString,
14
+ } from './is-length-bounded-string.mjs';
15
+
16
+ describe(isMinLengthString, () => {
17
+ test('should return true when the string is long enough', () => {
18
+ assert.isTrue(isMinLengthString('hello', 3));
19
+
20
+ assert.isTrue(isMinLengthString('hello', 5));
21
+
22
+ assert.isTrue(isMinLengthString('', 0));
23
+
24
+ assert.isTrue(isMinLengthString(' ', 1)); // Space counts
25
+ });
26
+
27
+ test('should return false when the string is too short', () => {
28
+ assert.isFalse(isMinLengthString('hi', 3));
29
+
30
+ assert.isFalse(isMinLengthString('', 1));
31
+ });
32
+
33
+ test('should measure length in UTF-16 code units', () => {
34
+ assert.isTrue(isMinLengthString('🎉', 2)); // Surrogate pair
35
+
36
+ assert.isFalse(isMinLengthString('🎉', 3));
37
+ });
38
+
39
+ test('should act as a type guard', () => {
40
+ const value: string = 'very-secret-password';
41
+
42
+ if (isMinLengthString(value, 12)) {
43
+ expectType<typeof value, MinLengthString<12>>('<=');
44
+
45
+ expectType<typeof value, NonEmptyString>('<='); // 12 >= 1
46
+
47
+ expectType<typeof value, MinLengthString<16>>('!<=');
48
+
49
+ expect(value.length).toBeGreaterThanOrEqual(12);
50
+ }
51
+ });
52
+
53
+ test('should preserve literal string types', () => {
54
+ const literal = 'hello';
55
+
56
+ if (isMinLengthString(literal, 3)) {
57
+ expectType<typeof literal, 'hello'>('<=');
58
+
59
+ expectType<typeof literal, MinLengthString<3>>('<=');
60
+ }
61
+ });
62
+ });
63
+
64
+ describe(isMaxLengthString, () => {
65
+ test('should return true when the string is short enough', () => {
66
+ assert.isTrue(isMaxLengthString('hello', 10));
67
+
68
+ assert.isTrue(isMaxLengthString('hello', 5));
69
+
70
+ assert.isTrue(isMaxLengthString('', 0));
71
+ });
72
+
73
+ test('should return false when the string is too long', () => {
74
+ assert.isFalse(isMaxLengthString('hello', 3));
75
+
76
+ assert.isFalse(isMaxLengthString(' ', 0));
77
+ });
78
+
79
+ test('should measure length in UTF-16 code units', () => {
80
+ assert.isFalse(isMaxLengthString('🎉', 1)); // Surrogate pair
81
+
82
+ assert.isTrue(isMaxLengthString('🎉', 2));
83
+ });
84
+
85
+ test('should act as a type guard', () => {
86
+ const value: string = 'noshiro';
87
+
88
+ if (isMaxLengthString(value, 32)) {
89
+ expectType<typeof value, MaxLengthString<32>>('<=');
90
+
91
+ expectType<typeof value, MaxLengthString<64>>('<='); // 32 <= 64
92
+
93
+ expectType<typeof value, MaxLengthString<16>>('!<=');
94
+
95
+ expect(value.length).toBeLessThanOrEqual(32);
96
+ }
97
+ });
98
+ });
99
+
100
+ describe(isBoundedLengthString, () => {
101
+ test('should return true when the length is within the range', () => {
102
+ assert.isTrue(isBoundedLengthString('user-12345678', 8, 16));
103
+
104
+ assert.isTrue(isBoundedLengthString('12345678', 8, 16)); // Lower bound (inclusive)
105
+
106
+ assert.isTrue(isBoundedLengthString('1234567890123456', 8, 16)); // Upper bound (inclusive)
107
+
108
+ assert.isTrue(isBoundedLengthString('', 0, 0));
109
+ });
110
+
111
+ test('should return false when the length is out of range', () => {
112
+ assert.isFalse(isBoundedLengthString('user', 8, 16));
113
+
114
+ assert.isFalse(isBoundedLengthString('12345678901234567', 8, 16));
115
+ });
116
+
117
+ test('should act as a type guard', () => {
118
+ const value: string = 'user-12345678';
119
+
120
+ if (isBoundedLengthString(value, 8, 16)) {
121
+ expectType<typeof value, BoundedLengthString<8, 16>>('<=');
122
+
123
+ expectType<typeof value, BoundedLengthString<1, 255>>('<='); // [8, 16] ⊆ [1, 255]
124
+
125
+ expectType<typeof value, MinLengthString<8>>('<=');
126
+
127
+ expectType<typeof value, MaxLengthString<16>>('<=');
128
+
129
+ expectType<typeof value, NonEmptyString>('<='); // 8 >= 1
130
+
131
+ expectType<typeof value, BoundedLengthString<10, 16>>('!<=');
132
+ }
133
+ });
134
+ });
135
+
136
+ describe(isFixedLengthString, () => {
137
+ test('should return true only for the exact length', () => {
138
+ assert.isTrue(isFixedLengthString('JP', 2));
139
+
140
+ assert.isTrue(isFixedLengthString('', 0));
141
+
142
+ assert.isFalse(isFixedLengthString('JPN', 2));
143
+
144
+ assert.isFalse(isFixedLengthString('J', 2));
145
+ });
146
+
147
+ test('should measure length in UTF-16 code units', () => {
148
+ assert.isTrue(isFixedLengthString('🎉', 2)); // Surrogate pair
149
+
150
+ assert.isFalse(isFixedLengthString('🎉', 1));
151
+ });
152
+
153
+ test('should act as a type guard', () => {
154
+ const value: string = 'JP';
155
+
156
+ if (isFixedLengthString(value, 2)) {
157
+ expectType<typeof value, FixedLengthString<2>>('<=');
158
+
159
+ expectType<typeof value, BoundedLengthString<2, 2>>('<=');
160
+
161
+ expectType<typeof value, MaxLengthString<5>>('<='); // 2 <= 5
162
+
163
+ expectType<typeof value, MinLengthString<1>>('<='); // 2 >= 1
164
+
165
+ expectType<typeof value, NonEmptyString>('<=');
166
+
167
+ expectType<typeof value, FixedLengthString<3>>('!<=');
168
+ }
169
+ });
170
+ });
@@ -13,8 +13,8 @@
13
13
  * - Excludes functions (they have `typeof` === `"function"`, not `"object"`)
14
14
  * - Includes arrays, dates, regex, and other object instances
15
15
  *
16
- * **Note:** This function returns `true` for arrays. If you need to check for
17
- * plain objects specifically (excluding arrays), use `isRecord()` instead.
16
+ * **Note:** This function returns `true` for arrays. If you need
17
+ * record-oriented narrowing that excludes arrays, use `isRecord()` instead.
18
18
  *
19
19
  * @example
20
20
  *
@@ -29,7 +29,7 @@
29
29
  * @param u - The value to check
30
30
  * @returns `true` if `u` is an object and not `null`, `false` otherwise. When
31
31
  * `true`, TypeScript narrows the type to `object`.
32
- * @see {@link isRecord} - For checking plain objects specifically (excludes arrays)
32
+ * @see {@link isRecord} - For record-oriented type narrowing (excludes arrays)
33
33
  */
34
34
  // eslint-disable-next-line @typescript-eslint/no-restricted-types
35
35
  export const isNonNullObject = (u: unknown): u is object =>
@@ -2,31 +2,46 @@ import { type MutableRecord, type UnknownRecord } from 'ts-type-forge';
2
2
  import { isNonNullObject } from './is-non-null-object.mjs';
3
3
 
4
4
  /**
5
- * Type guard that checks if a value is a plain object (record) - a non-null
6
- * object that is not an array.
5
+ * Type guard that checks if a value is a record — a non-null, non-array
6
+ * object — and narrows it to `UnknownRecord` (= `ReadonlyRecord<string,
7
+ * unknown>`).
7
8
  *
8
- * This function is useful for identifying "plain" JavaScript objects (also
9
- * called records or dictionaries) - objects that are typically used as
10
- * key-value collections. It excludes arrays, functions, and special object
11
- * types like Date, RegExp, etc., focusing on objects that can be safely treated
12
- * as property collections.
9
+ * This function is designed to be combined with {@link hasKey} to probe
10
+ * values of type `unknown` (e.g. parsed JSON, caught errors, external data)
11
+ * in a type-safe way: `if (isRecord(u) && hasKey(u, 'some-key')) { ... }`.
13
12
  *
14
13
  * **Type Narrowing Behavior:**
15
14
  *
16
- * - Narrows `unknown` to `UnknownRecord` (equivalent to `Record<PropertyKey,
17
- * unknown>`)
18
- * - Excludes `null`, `undefined`, primitives, arrays, and functions
19
- * - Returns `true` for plain objects `{}`, object literals, and objects created
20
- * with `Object.create()`
21
- * - Returns `false` for arrays, even though they are technically objects
22
- *
23
- * **Implementation:** Uses `isNonNullObject()` to check for objects, then
24
- * `Array.isArray()` to exclude arrays.
15
+ * - Narrows `unknown` to `UnknownRecord` (= `ReadonlyRecord<string, unknown>`)
16
+ * - Excludes `null`, `undefined`, primitives, and functions
17
+ * - Excludes arrays: arrays have no string index signature, so they are not
18
+ * assignable to `UnknownRecord`; admitting them at runtime would be
19
+ * inconsistent with the narrowed type
20
+ * - Returns `true` for every other non-null object, including dictionary
21
+ * objects created with `Object.create(null)` and instances such as `Date`,
22
+ * `RegExp`, `Map`, `Set`, `Error`, and user-defined classes
23
+ *
24
+ * **Why non-plain objects are included:** since every property of
25
+ * `UnknownRecord` is typed `unknown`, reading properties from any object
26
+ * through this narrowing is type-safe, and `hasKey` (backed by
27
+ * `Object.hasOwn`) answers own-property questions correctly on any object
28
+ * (e.g. `Map` entries are not own properties, so `hasKey` correctly returns
29
+ * `false` for them). Restricting the guard to plain objects would require
30
+ * prototype checks that misjudge cross-realm objects and would prevent
31
+ * probing useful instances such as `Error` subclasses.
32
+ *
33
+ * **Implementation:** `isNonNullObject(u) && !Array.isArray(u)`.
25
34
  *
26
35
  * @example
27
36
  *
28
37
  * ```ts
29
- * const entries: readonly unknown[] = [{ id: 1 }, 'str', 0, null] as const;
38
+ * const entries: readonly unknown[] = [
39
+ * { id: 1 },
40
+ * [1, 2],
41
+ * 'str',
42
+ * 0,
43
+ * null,
44
+ * ] as const;
30
45
  *
31
46
  * const records = entries.filter(isRecord);
32
47
  *
@@ -34,30 +49,33 @@ import { isNonNullObject } from './is-non-null-object.mjs';
34
49
  * ```
35
50
  *
36
51
  * @param u - The value to check
37
- * @returns `true` if `u` is a non-null object and not an array, `false`
38
- * otherwise. When `true`, TypeScript narrows the type to `UnknownRecord`.
39
- * @see {@link isNonNullObject} - For checking any object type (includes arrays)
52
+ * @returns `true` if `u` is a non-null, non-array object, `false` otherwise.
53
+ * When `true`, TypeScript narrows the type to `UnknownRecord`.
54
+ * @see {@link isNonNullObject} - For checking any non-null object (includes arrays)
40
55
  * @see {@link hasKey} - For checking if a record has specific keys
41
56
  */
42
- export const isRecord = (u: unknown): u is UnknownRecord => isNonNullObject(u);
57
+ export const isRecord = (u: unknown): u is UnknownRecord =>
58
+ isNonNullObject(u) && !Array.isArray(u);
43
59
 
44
60
  /**
45
- * Type guard that checks if a value is a mutable record (an object with mutable string keys).
61
+ * Type guard that checks if a value is a mutable record.
46
62
  *
47
- * This function is an alias for {@link isRecord}, but narrows the type to
48
- * `MutableRecord<string, unknown>`, which is useful when you need to ensure
49
- * that the object can be safely mutated (i.e., its properties can be added or changed).
63
+ * This function has the same runtime behavior as {@link isRecord} (non-null,
64
+ * non-array object check), but narrows the type to `MutableRecord<string,
65
+ * unknown>`, which is useful when you need to add or overwrite properties on
66
+ * the narrowed object.
50
67
  *
51
68
  * **Type Narrowing Behavior:**
52
69
  *
53
70
  * - Narrows `unknown` to `MutableRecord<string, unknown>`
54
- * - Excludes `null`, `undefined`, primitives, arrays, and functions
55
- * - Returns `true` for plain objects with mutable string keys
56
- * - Returns `false` for arrays and other non-record types
71
+ * - Excludes `null`, `undefined`, primitives, functions, and arrays
72
+ * - Returns `true` for every other non-null object (see {@link isRecord} for
73
+ * details on non-plain objects such as `Date`, `Map`, and `Set`)
57
74
  *
58
75
  * **Implementation:** This is a type alias for {@link isRecord}.
59
76
  *
60
77
  * @example
78
+ *
61
79
  * ```ts
62
80
  * const obj: unknown = { foo: 1 } as const;
63
81
  *
@@ -67,8 +85,9 @@ export const isRecord = (u: unknown): u is UnknownRecord => isNonNullObject(u);
67
85
  * ```
68
86
  *
69
87
  * @param u - The value to check
70
- * @returns `true` if `u` is a mutable record (plain object with string keys), `false` otherwise.
71
- * When `true`, TypeScript narrows the type to `MutableRecord<string, unknown>`.
88
+ * @returns `true` if `u` is a non-null, non-array object, `false` otherwise.
89
+ * When `true`, TypeScript narrows the type to `MutableRecord<string,
90
+ * unknown>`.
72
91
  * @see {@link isRecord} - For the underlying implementation and more details
73
92
  */
74
93
  export const isMutableRecord: (