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.
- package/dist/array/impl/array-utils-transformation.d.mts.map +1 -1
- package/dist/array/impl/array-utils-transformation.mjs.map +1 -1
- package/dist/entry-point.mjs +1 -0
- package/dist/entry-point.mjs.map +1 -1
- package/dist/expect-type.d.mts.map +1 -1
- package/dist/expect-type.mjs.map +1 -1
- package/dist/functional/ternary-result/ternary-result.d.mts.map +1 -1
- package/dist/guard/index.d.mts +1 -0
- package/dist/guard/index.d.mts.map +1 -1
- package/dist/guard/index.mjs +1 -0
- package/dist/guard/index.mjs.map +1 -1
- package/dist/guard/is-length-bounded-string.d.mts +164 -0
- package/dist/guard/is-length-bounded-string.d.mts.map +1 -0
- package/dist/guard/is-length-bounded-string.mjs +164 -0
- package/dist/guard/is-length-bounded-string.mjs.map +1 -0
- package/dist/guard/is-non-null-object.d.mts +3 -3
- package/dist/guard/is-non-null-object.mjs +3 -3
- package/dist/guard/is-record.d.mts +47 -29
- package/dist/guard/is-record.d.mts.map +1 -1
- package/dist/guard/is-record.mjs +48 -30
- package/dist/guard/is-record.mjs.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/number/branded-types/int.d.mts +1 -1
- package/dist/number/branded-types/int16.d.mts +1 -1
- package/dist/number/branded-types/int32.d.mts +1 -1
- package/dist/number/branded-types/non-negative-int16.d.mts +1 -1
- package/dist/number/branded-types/non-negative-int32.d.mts +1 -1
- package/dist/number/branded-types/positive-int.d.mts +1 -1
- package/dist/number/branded-types/positive-int16.d.mts +1 -1
- package/dist/number/branded-types/positive-int32.d.mts +1 -1
- package/dist/number/branded-types/positive-safe-int.d.mts +1 -1
- package/dist/number/branded-types/positive-uint16.d.mts +1 -1
- package/dist/number/branded-types/positive-uint32.d.mts +1 -1
- package/dist/number/branded-types/safe-int.d.mts +1 -1
- package/dist/number/branded-types/safe-uint.d.mts +1 -1
- package/dist/number/branded-types/uint.d.mts +1 -1
- package/dist/number/branded-types/uint16.d.mts +1 -1
- package/dist/number/branded-types/uint32.d.mts +1 -1
- package/dist/number/refined-number-utils.d.mts.map +1 -1
- package/dist/number/refined-number-utils.mjs.map +1 -1
- package/dist/object/object.d.mts.map +1 -1
- package/dist/object/object.mjs.map +1 -1
- package/dist/others/fast-deep-equal.d.mts.map +1 -1
- package/dist/others/fast-deep-equal.mjs +7 -5
- package/dist/others/fast-deep-equal.mjs.map +1 -1
- package/package.json +12 -12
- package/src/array/impl/array-utils-transformation.mts +1 -2
- package/src/array/impl/array-utils-transformation.test.mts +3 -36
- package/src/array/impl/array-utils-validation.test.mts +1 -3
- package/src/expect-type.mts +4 -5
- package/src/functional/ternary-result/ternary-result.mts +2 -6
- package/src/guard/index.mts +1 -0
- package/src/guard/is-length-bounded-string.mts +199 -0
- package/src/guard/is-length-bounded-string.test.mts +170 -0
- package/src/guard/is-non-null-object.mts +3 -3
- package/src/guard/is-record.mts +49 -30
- package/src/guard/is-record.test.mts +133 -61
- package/src/number/refined-number-utils.mts +1 -9
- package/src/object/object.mts +25 -23
- 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
|
-
|
|
|
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
|
-
|
|
|
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
|
-
|
|
|
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
|
|
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
|
|
package/src/expect-type.mts
CHANGED
|
@@ -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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
|
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';
|
package/src/guard/index.mts
CHANGED
|
@@ -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
|
|
17
|
-
*
|
|
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
|
|
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 =>
|
package/src/guard/is-record.mts
CHANGED
|
@@ -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
|
|
6
|
-
* object
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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` (
|
|
17
|
-
*
|
|
18
|
-
* - Excludes
|
|
19
|
-
*
|
|
20
|
-
* with
|
|
21
|
-
* - Returns `
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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[] = [
|
|
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
|
|
38
|
-
*
|
|
39
|
-
* @see {@link isNonNullObject} - For checking any object
|
|
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 =>
|
|
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
|
|
61
|
+
* Type guard that checks if a value is a mutable record.
|
|
46
62
|
*
|
|
47
|
-
* This function
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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,
|
|
55
|
-
* - Returns `true` for
|
|
56
|
-
* -
|
|
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
|
|
71
|
-
* When `true`, TypeScript narrows the type to `MutableRecord<string,
|
|
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: (
|