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,38 @@
|
|
|
1
|
+
/// <reference types="ts-type-forge" />
|
|
2
|
+
|
|
3
|
+
type SmallUint = SmallInt<'>=0'>;
|
|
4
|
+
type PositiveSmallInt = SmallInt<'>0'>;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Represents the type of keys that can be used in a standard JavaScript Map.
|
|
8
|
+
*/
|
|
9
|
+
type MapSetKeyType = Primitive;
|
|
10
|
+
|
|
11
|
+
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/length
|
|
12
|
+
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/length
|
|
13
|
+
// Max array length : 2^32 - 1
|
|
14
|
+
// Max string length : 2^53 - 1
|
|
15
|
+
|
|
16
|
+
declare namespace SizeType {
|
|
17
|
+
type Arr = Uint32;
|
|
18
|
+
type TypedArray = SafeUint;
|
|
19
|
+
type Str = SafeUint;
|
|
20
|
+
|
|
21
|
+
type ArrSearchResult = Arr | -1;
|
|
22
|
+
type TypedArraySearchResult = TypedArray | -1;
|
|
23
|
+
type StrSearchResult = Str | -1;
|
|
24
|
+
|
|
25
|
+
type ArgArr = WithSmallInt<NormalizeBrandUnion<NegativeInt32 | Arr>>;
|
|
26
|
+
type ArgTypedArray = WithSmallInt<SafeInt>;
|
|
27
|
+
type ArgStr = WithSmallInt<SafeInt>;
|
|
28
|
+
|
|
29
|
+
type ArgArrPositive = WithSmallInt<IntersectBrand<PositiveNumber, Arr>>;
|
|
30
|
+
type ArgTypedArrayPositive = WithSmallInt<
|
|
31
|
+
IntersectBrand<PositiveNumber, TypedArray>
|
|
32
|
+
>;
|
|
33
|
+
type ArgStrPositive = WithSmallInt<IntersectBrand<PositiveNumber, Str>>;
|
|
34
|
+
|
|
35
|
+
type ArgArrNonNegative = WithSmallInt<Arr>;
|
|
36
|
+
type ArgTypedArrayNonNegative = WithSmallInt<TypedArray>;
|
|
37
|
+
type ArgStrNonNegative = WithSmallInt<Str>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard function that checks if an object has a specific key as its own property.
|
|
3
|
+
*
|
|
4
|
+
* This function uses `Object.hasOwn()` to check if the given object has the specified key
|
|
5
|
+
* as its own property (not inherited). It acts as a type guard that narrows the type of the
|
|
6
|
+
* object to guarantee the key exists, enabling type-safe property access.
|
|
7
|
+
*
|
|
8
|
+
* **Type Narrowing Behavior:**
|
|
9
|
+
* - When the guard returns `true`, TypeScript narrows the object type to include the checked key
|
|
10
|
+
* - For union types, only union members that contain the key are preserved
|
|
11
|
+
* - The key's value type is preserved from the original object type when possible
|
|
12
|
+
*
|
|
13
|
+
* @template R - The type of the input object, must extend UnknownRecord
|
|
14
|
+
* @template K - The type of the key to check for, must extend PropertyKey (string | number | symbol)
|
|
15
|
+
* @param obj - The object to check for the presence of the key
|
|
16
|
+
* @param key - The key to check for in the object
|
|
17
|
+
* @returns `true` if the object has the specified key as its own property, `false` otherwise.
|
|
18
|
+
* When `true`, TypeScript narrows the object type to guarantee the key exists.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* Basic usage with known object structure:
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const obj = { a: 1, b: 'hello' };
|
|
24
|
+
*
|
|
25
|
+
* if (hasKey(obj, 'a')) {
|
|
26
|
+
* // obj is narrowed to guarantee 'a' exists
|
|
27
|
+
* console.log(obj.a); // TypeScript knows 'a' exists and is type number
|
|
28
|
+
* // No need for optional chaining or undefined checks
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* if (hasKey(obj, 'c')) {
|
|
32
|
+
* // This block won't execute at runtime
|
|
33
|
+
* console.log(obj.c); // But TypeScript would know 'c' exists if it did
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* Working with dynamic objects and unknown keys:
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const dynamicObj: Record<string, unknown> = { x: 10, y: 20 };
|
|
41
|
+
* const userInput: string = getUserInput();
|
|
42
|
+
*
|
|
43
|
+
* if (hasKey(dynamicObj, userInput)) {
|
|
44
|
+
* // Safe to access the dynamic key
|
|
45
|
+
* const value = dynamicObj[userInput]; // Type: unknown
|
|
46
|
+
* console.log(`Value for ${userInput}:`, value);
|
|
47
|
+
* } else {
|
|
48
|
+
* console.log(`Key '${userInput}' not found`);
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* Type narrowing with union types:
|
|
54
|
+
* ```typescript
|
|
55
|
+
* type UserPreferences =
|
|
56
|
+
* | { theme: 'dark'; notifications: boolean }
|
|
57
|
+
* | { theme: 'light' }
|
|
58
|
+
* | { autoSave: true; interval: number };
|
|
59
|
+
*
|
|
60
|
+
* const preferences: UserPreferences = getPreferences();
|
|
61
|
+
*
|
|
62
|
+
* if (hasKey(preferences, 'theme')) {
|
|
63
|
+
* // preferences is narrowed to the first two union members
|
|
64
|
+
* console.log(preferences.theme); // 'dark' | 'light'
|
|
65
|
+
* }
|
|
66
|
+
*
|
|
67
|
+
* if (hasKey(preferences, 'autoSave')) {
|
|
68
|
+
* // preferences is narrowed to the third union member
|
|
69
|
+
* console.log(preferences.interval); // number (we know this exists)
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* Combining with other type guards for progressive narrowing:
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const data: unknown = parseApiResponse();
|
|
77
|
+
*
|
|
78
|
+
* if (isRecord(data) && hasKey(data, 'user')) {
|
|
79
|
+
* // data is now Record<string, unknown> with guaranteed 'user' key
|
|
80
|
+
* const user = data.user;
|
|
81
|
+
*
|
|
82
|
+
* if (isRecord(user) && hasKey(user, 'name')) {
|
|
83
|
+
* // Safely access nested properties
|
|
84
|
+
* console.log('User name:', user.name);
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* @see {@link keyIsIn} - Similar function that narrows the key type instead of the object type
|
|
90
|
+
*/
|
|
91
|
+
export const hasKey = <
|
|
92
|
+
const R extends UnknownRecord,
|
|
93
|
+
const K extends PropertyKey,
|
|
94
|
+
>(
|
|
95
|
+
obj: R,
|
|
96
|
+
key: K,
|
|
97
|
+
): obj is HasKeyReturnType<R, K> => Object.hasOwn(obj, key);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @internal
|
|
101
|
+
* When R is a union type (including the case with only one element), if any element in the union
|
|
102
|
+
* contains K as a key, returns a type that narrows the union to only those elements that contain K as a key.
|
|
103
|
+
* If none of the elements in the union contain K as a key, returns `ReadonlyRecord<K, unknown>`.
|
|
104
|
+
* The result is made readonly.
|
|
105
|
+
*/
|
|
106
|
+
export type HasKeyReturnType<
|
|
107
|
+
R extends UnknownRecord,
|
|
108
|
+
K extends PropertyKey,
|
|
109
|
+
> = R extends R // union distribution
|
|
110
|
+
? K extends keyof R
|
|
111
|
+
? string extends keyof R
|
|
112
|
+
? ReadonlyRecord<K, R[keyof R]> & R
|
|
113
|
+
: number extends keyof R
|
|
114
|
+
? ReadonlyRecord<K, R[keyof R]> & R
|
|
115
|
+
: symbol extends keyof R
|
|
116
|
+
? ReadonlyRecord<K, R[keyof R]> & R
|
|
117
|
+
: R
|
|
118
|
+
: never // omit union member that does not have key K
|
|
119
|
+
: never; // dummy case for union distribution
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { hasKey, type HasKeyReturnType } from './has-key.mjs';
|
|
3
|
+
|
|
4
|
+
{
|
|
5
|
+
expectType<
|
|
6
|
+
HasKeyReturnType<Readonly<{ a: 0 }> | Readonly<{ b: 1 }>, 'a'>,
|
|
7
|
+
Readonly<{ a: 0 }>
|
|
8
|
+
>('=');
|
|
9
|
+
|
|
10
|
+
expectType<
|
|
11
|
+
HasKeyReturnType<Readonly<{ a: 0 }> | Readonly<{ b: 1 }>, 'b'>,
|
|
12
|
+
Readonly<{ b: 1 }>
|
|
13
|
+
>('=');
|
|
14
|
+
|
|
15
|
+
expectType<
|
|
16
|
+
HasKeyReturnType<Readonly<{ a: 0 }> | Readonly<{ b: 1 }>, 'd'>,
|
|
17
|
+
never
|
|
18
|
+
>('=');
|
|
19
|
+
|
|
20
|
+
expectType<
|
|
21
|
+
HasKeyReturnType<
|
|
22
|
+
| Readonly<{ a: 0 }>
|
|
23
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
24
|
+
| Readonly<{ b: 2 }>
|
|
25
|
+
| Readonly<{ c: 3 }>,
|
|
26
|
+
'a'
|
|
27
|
+
>,
|
|
28
|
+
Readonly<{ a: 0 }> | Readonly<{ a: 1; b: 1 }>
|
|
29
|
+
>('=');
|
|
30
|
+
|
|
31
|
+
expectType<
|
|
32
|
+
HasKeyReturnType<
|
|
33
|
+
| Readonly<{ a: 0 }>
|
|
34
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
35
|
+
| Readonly<{ b: 2 }>
|
|
36
|
+
| Readonly<{ c: 3 }>,
|
|
37
|
+
'b'
|
|
38
|
+
>,
|
|
39
|
+
Readonly<{ a: 1; b: 1 }> | Readonly<{ b: 2 }>
|
|
40
|
+
>('=');
|
|
41
|
+
|
|
42
|
+
expectType<
|
|
43
|
+
HasKeyReturnType<
|
|
44
|
+
| ReadonlyRecord<string, number>
|
|
45
|
+
| Readonly<{ a: 0 }>
|
|
46
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
47
|
+
| Readonly<{ b: 2 }>,
|
|
48
|
+
'a'
|
|
49
|
+
>,
|
|
50
|
+
| Readonly<{ a: 0 }>
|
|
51
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
52
|
+
| (ReadonlyRecord<'a', number> & ReadonlyRecord<string, number>)
|
|
53
|
+
>('=');
|
|
54
|
+
|
|
55
|
+
expectType<
|
|
56
|
+
HasKeyReturnType<ReadonlyRecord<string, unknown>, 'a'>,
|
|
57
|
+
ReadonlyRecord<'a', unknown> & ReadonlyRecord<string, unknown>
|
|
58
|
+
>('=');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
{
|
|
62
|
+
type R = Readonly<{ a: 0 }> | Readonly<{ b: 1 }>;
|
|
63
|
+
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
65
|
+
const obj: R = { a: 0 } as R;
|
|
66
|
+
|
|
67
|
+
if (hasKey(obj, 'a')) {
|
|
68
|
+
expectType<typeof obj.a, 0>('=');
|
|
69
|
+
expectType<typeof obj, Readonly<{ a: 0 }>>('=');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (hasKey(obj, 'c')) {
|
|
73
|
+
expectType<typeof obj, never>('=');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
{
|
|
78
|
+
type R =
|
|
79
|
+
| Readonly<{ a: 0 }>
|
|
80
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
81
|
+
| Readonly<{ b: 2 }>
|
|
82
|
+
| Readonly<{ c: 3 }>;
|
|
83
|
+
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
85
|
+
const obj: R = { a: 0 } as R;
|
|
86
|
+
|
|
87
|
+
if (hasKey(obj, 'a') && hasKey(obj, 'b')) {
|
|
88
|
+
expectType<typeof obj.a, 1>('=');
|
|
89
|
+
expectType<typeof obj.b, 1>('=');
|
|
90
|
+
expectType<typeof obj, Readonly<{ a: 1; b: 1 }>>('=');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
type R =
|
|
96
|
+
| ReadonlyRecord<string, number>
|
|
97
|
+
| Readonly<{ a: 0 }>
|
|
98
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
99
|
+
| Readonly<{ b: 2 }>;
|
|
100
|
+
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
102
|
+
const obj: R = { a: 0 } as R;
|
|
103
|
+
|
|
104
|
+
expectType<
|
|
105
|
+
R,
|
|
106
|
+
| ReadonlyRecord<string, number>
|
|
107
|
+
| Readonly<{ a: 0 }>
|
|
108
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
109
|
+
| Readonly<{ b: 2 }>
|
|
110
|
+
>('=');
|
|
111
|
+
|
|
112
|
+
if (hasKey(obj, 'a')) {
|
|
113
|
+
expectType<typeof obj.a, number>('=');
|
|
114
|
+
|
|
115
|
+
expectType<
|
|
116
|
+
typeof obj,
|
|
117
|
+
| Readonly<{ a: 0 }>
|
|
118
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
119
|
+
| (ReadonlyRecord<'a', number> & ReadonlyRecord<string, number>)
|
|
120
|
+
>('=');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (hasKey(obj, 'a') && hasKey(obj, 'b')) {
|
|
124
|
+
expectType<typeof obj.a, number>('=');
|
|
125
|
+
expectType<typeof obj.b, number>('=');
|
|
126
|
+
expectType<
|
|
127
|
+
typeof obj,
|
|
128
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
129
|
+
| (ReadonlyRecord<'a', number> &
|
|
130
|
+
ReadonlyRecord<'b', number> &
|
|
131
|
+
ReadonlyRecord<string, number>)
|
|
132
|
+
>('=');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
{
|
|
137
|
+
const o: ReadonlyRecord<string, unknown> = { a: 0, b: 1 };
|
|
138
|
+
|
|
139
|
+
if (hasKey(o, 'a')) {
|
|
140
|
+
expectType<typeof o.a, unknown>('=');
|
|
141
|
+
expectType<
|
|
142
|
+
typeof o,
|
|
143
|
+
ReadonlyRecord<'a', unknown> & ReadonlyRecord<string, unknown>
|
|
144
|
+
>('=');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (hasKey(o, 'c')) {
|
|
148
|
+
expectType<typeof o.c, unknown>('=');
|
|
149
|
+
expectType<
|
|
150
|
+
typeof o,
|
|
151
|
+
ReadonlyRecord<'c', unknown> & ReadonlyRecord<string, unknown>
|
|
152
|
+
>('=');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (hasKey(o, 'a') && hasKey(o, 'b')) {
|
|
156
|
+
expectType<typeof o.a, unknown>('=');
|
|
157
|
+
expectType<typeof o.b, unknown>('=');
|
|
158
|
+
expectType<
|
|
159
|
+
typeof o,
|
|
160
|
+
ReadonlyRecord<'a', unknown> &
|
|
161
|
+
ReadonlyRecord<'b', unknown> &
|
|
162
|
+
ReadonlyRecord<string, unknown>
|
|
163
|
+
>('=');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
/**
|
|
168
|
+
* @note Simply using `R & Record<K, R[K]>` as the return type of hasKey may seem good enough
|
|
169
|
+
* since it works as intended for cases like `obj: Record<string, unknown>`.
|
|
170
|
+
* However, for finite-sized types like `obj: { a: 0 } | { b: 1 }`,
|
|
171
|
+
* filtering with `hasKey(obj, 'a')` causes `obj.a` to widen to `unknown` instead of `0`,
|
|
172
|
+
* which doesn't work well.
|
|
173
|
+
*/
|
|
174
|
+
|
|
175
|
+
const hasOwnNaive = <R extends UnknownRecord, K extends string>(
|
|
176
|
+
obj: R,
|
|
177
|
+
key: K,
|
|
178
|
+
): obj is R & Record<K, R[K]> => hasKey(obj, key);
|
|
179
|
+
|
|
180
|
+
{
|
|
181
|
+
type O =
|
|
182
|
+
| Readonly<{ a: 0 }>
|
|
183
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
184
|
+
| Readonly<{ b: 2 }>
|
|
185
|
+
| Record<string, number>;
|
|
186
|
+
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
188
|
+
const o2 = { b: 2 } as O;
|
|
189
|
+
|
|
190
|
+
if (hasOwnNaive(o2, 'a')) {
|
|
191
|
+
expectType<typeof o2.a, unknown>('=');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
195
|
+
if ('a' in o2) {
|
|
196
|
+
expectType<typeof o2.a, number>('=');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
{
|
|
201
|
+
type O =
|
|
202
|
+
| Readonly<{ a: 0 }>
|
|
203
|
+
| Readonly<{ a: 1; b: 1 }>
|
|
204
|
+
| Readonly<{ b: 2 }>;
|
|
205
|
+
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
207
|
+
const o2 = { b: 2 } as O;
|
|
208
|
+
|
|
209
|
+
if (hasOwnNaive(o2, 'a')) {
|
|
210
|
+
expectType<typeof o2.a, unknown>('=');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
214
|
+
if ('a' in o2) {
|
|
215
|
+
expectType<typeof o2.a, 0 | 1>('=');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard that checks if a value is a non-empty string.
|
|
3
|
+
*
|
|
4
|
+
* This function performs both a type check (ensuring the value is a string) and a length check
|
|
5
|
+
* (ensuring the string is not empty). It acts as a type guard that narrows the input type to
|
|
6
|
+
* exclude `null`, `undefined`, and empty strings.
|
|
7
|
+
*
|
|
8
|
+
* **Type Narrowing Behavior:**
|
|
9
|
+
* - Eliminates `null` and `undefined` from the type
|
|
10
|
+
* - Excludes empty string (`""`) from the type
|
|
11
|
+
* - Preserves literal string types when possible
|
|
12
|
+
* - Whitespace-only strings are considered non-empty
|
|
13
|
+
*
|
|
14
|
+
* @template S - The input type, which can include `string`, `null`, or `undefined`
|
|
15
|
+
* @param s - The value to check
|
|
16
|
+
* @returns `true` if `s` is a string with length > 0, `false` otherwise.
|
|
17
|
+
* When `true`, TypeScript narrows the type to exclude empty strings and nullish values.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* Basic usage with different string types:
|
|
21
|
+
* ```typescript
|
|
22
|
+
* isNonEmptyString("hello"); // true
|
|
23
|
+
* isNonEmptyString(" "); // true (whitespace is considered non-empty)
|
|
24
|
+
* isNonEmptyString("\t\n"); // true (whitespace characters are non-empty)
|
|
25
|
+
* isNonEmptyString(""); // false
|
|
26
|
+
* isNonEmptyString(null); // false
|
|
27
|
+
* isNonEmptyString(undefined); // false
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* Type guard usage with nullable strings:
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const userInput: string | null | undefined = getUserInput();
|
|
34
|
+
*
|
|
35
|
+
* if (isNonEmptyString(userInput)) {
|
|
36
|
+
* // userInput is now typed as non-empty string
|
|
37
|
+
* console.log(userInput.charAt(0)); // Safe to access string methods
|
|
38
|
+
* console.log(userInput.toUpperCase()); // No need for null checks
|
|
39
|
+
* const length = userInput.length; // Guaranteed to be > 0
|
|
40
|
+
* } else {
|
|
41
|
+
* console.log('No valid input provided');
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* Working with literal string types:
|
|
47
|
+
* ```typescript
|
|
48
|
+
* type Status = "active" | "inactive" | "" | null;
|
|
49
|
+
* const status: Status = getStatus();
|
|
50
|
+
*
|
|
51
|
+
* if (isNonEmptyString(status)) {
|
|
52
|
+
* // status is now typed as "active" | "inactive"
|
|
53
|
+
* console.log(`Status is: ${status}`);
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* Form validation patterns:
|
|
59
|
+
* ```typescript
|
|
60
|
+
* interface FormData {
|
|
61
|
+
* name?: string;
|
|
62
|
+
* email?: string;
|
|
63
|
+
* phone?: string;
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* function validateForm(data: FormData): string[] {
|
|
67
|
+
* const errors: string[] = [];
|
|
68
|
+
*
|
|
69
|
+
* if (!isNonEmptyString(data.name)) {
|
|
70
|
+
* errors.push('Name is required');
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* if (!isNonEmptyString(data.email)) {
|
|
74
|
+
* errors.push('Email is required');
|
|
75
|
+
* } else if (!data.email.includes('@')) {
|
|
76
|
+
* // Safe to access string methods after guard
|
|
77
|
+
* errors.push('Invalid email format');
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* return errors;
|
|
81
|
+
* }
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* Filtering arrays of potentially empty strings:
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const responses: (string | null | undefined)[] = [
|
|
88
|
+
* "hello",
|
|
89
|
+
* "",
|
|
90
|
+
* "world",
|
|
91
|
+
* null,
|
|
92
|
+
* undefined,
|
|
93
|
+
* " "
|
|
94
|
+
* ];
|
|
95
|
+
*
|
|
96
|
+
* const validResponses = responses.filter(isNonEmptyString);
|
|
97
|
+
* // validResponses is now string[] containing ["hello", "world", " "]
|
|
98
|
+
*
|
|
99
|
+
* validResponses.forEach(response => {
|
|
100
|
+
* // Each response is guaranteed to be a non-empty string
|
|
101
|
+
* console.log(response.trim().toUpperCase());
|
|
102
|
+
* });
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export const isNonEmptyString = <S extends string | null | undefined>(
|
|
106
|
+
s: S,
|
|
107
|
+
): s is RelaxedExclude<NonNullable<S>, ''> =>
|
|
108
|
+
typeof s === 'string' && s.length > 0;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { expectType } from '../expect-type.mjs';
|
|
2
|
+
import { isNonEmptyString } from './is-non-empty-string.mjs';
|
|
3
|
+
|
|
4
|
+
describe('isNonEmptyString', () => {
|
|
5
|
+
test('should return true for non-empty strings', () => {
|
|
6
|
+
expect(isNonEmptyString('hello')).toBe(true);
|
|
7
|
+
expect(isNonEmptyString('a')).toBe(true);
|
|
8
|
+
expect(isNonEmptyString(' ')).toBe(true); // Space is not empty
|
|
9
|
+
expect(isNonEmptyString(' multiple spaces ')).toBe(true);
|
|
10
|
+
expect(isNonEmptyString('123')).toBe(true);
|
|
11
|
+
expect(isNonEmptyString('special!@#$%')).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should return false for empty string', () => {
|
|
15
|
+
expect(isNonEmptyString('')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should return false for non-string values', () => {
|
|
19
|
+
expect(isNonEmptyString(null)).toBe(false);
|
|
20
|
+
expect(isNonEmptyString(undefined)).toBe(false);
|
|
21
|
+
// @ts-expect-error Testing non-string types
|
|
22
|
+
expect(isNonEmptyString(42)).toBe(false);
|
|
23
|
+
// @ts-expect-error Testing non-string types
|
|
24
|
+
expect(isNonEmptyString(0)).toBe(false);
|
|
25
|
+
// @ts-expect-error Testing non-string types
|
|
26
|
+
expect(isNonEmptyString(true)).toBe(false);
|
|
27
|
+
// @ts-expect-error Testing non-string types
|
|
28
|
+
expect(isNonEmptyString(false)).toBe(false);
|
|
29
|
+
// @ts-expect-error Testing non-string types
|
|
30
|
+
expect(isNonEmptyString({})).toBe(false);
|
|
31
|
+
// @ts-expect-error Testing non-string types
|
|
32
|
+
expect(isNonEmptyString([])).toBe(false);
|
|
33
|
+
// @ts-expect-error Testing non-string types
|
|
34
|
+
expect(isNonEmptyString(['string'])).toBe(false);
|
|
35
|
+
// @ts-expect-error Testing non-string types
|
|
36
|
+
expect(isNonEmptyString(() => 'string')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('should act as a type guard', () => {
|
|
40
|
+
const value: unknown = 'test';
|
|
41
|
+
|
|
42
|
+
// @ts-expect-error Testing non-string types
|
|
43
|
+
if (isNonEmptyString(value)) {
|
|
44
|
+
expectType<typeof value, string>('=');
|
|
45
|
+
// TypeScript knows it's a string
|
|
46
|
+
expect(value.length).toBeGreaterThan(0);
|
|
47
|
+
expect(value.charAt(0)).toBe('t');
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should narrow string | undefined | null types', () => {
|
|
52
|
+
const maybeString: string | undefined | null = 'hello';
|
|
53
|
+
|
|
54
|
+
if (isNonEmptyString(maybeString)) {
|
|
55
|
+
expectType<typeof maybeString, string>('=');
|
|
56
|
+
expect(maybeString.toUpperCase()).toBe('HELLO');
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should work in filter operations', () => {
|
|
61
|
+
const mixed: unknown[] = [
|
|
62
|
+
'valid',
|
|
63
|
+
'',
|
|
64
|
+
null,
|
|
65
|
+
'another',
|
|
66
|
+
42,
|
|
67
|
+
undefined,
|
|
68
|
+
'third',
|
|
69
|
+
false,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
// @ts-expect-error Testing non-string types
|
|
73
|
+
const nonEmptyStrings = mixed.filter(isNonEmptyString);
|
|
74
|
+
expect(nonEmptyStrings).toStrictEqual(['valid', 'another', 'third']);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should handle string edge cases', () => {
|
|
78
|
+
expect(isNonEmptyString('\n')).toBe(true); // Newline
|
|
79
|
+
expect(isNonEmptyString('\t')).toBe(true); // Tab
|
|
80
|
+
expect(isNonEmptyString('\r')).toBe(true); // Carriage return
|
|
81
|
+
expect(isNonEmptyString('\0')).toBe(true); // Null character
|
|
82
|
+
expect(isNonEmptyString('🎉')).toBe(true); // Emoji
|
|
83
|
+
expect(isNonEmptyString('ä½ å¥½')).toBe(true); // Unicode characters
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should not accept String objects', () => {
|
|
87
|
+
// @ts-expect-error Testing non-string types
|
|
88
|
+
// eslint-disable-next-line no-new-wrappers
|
|
89
|
+
expect(isNonEmptyString(new String('hello') as unknown)).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type guard that checks if a value is a non-null object.
|
|
3
|
+
*
|
|
4
|
+
* This function checks if a value has type `"object"` according to the `typeof` operator
|
|
5
|
+
* and is not `null`. This includes all object types such as plain objects, arrays, dates,
|
|
6
|
+
* regular expressions, and other object instances, but excludes functions.
|
|
7
|
+
*
|
|
8
|
+
* **Type Narrowing Behavior:**
|
|
9
|
+
* - Narrows `unknown` to `object`
|
|
10
|
+
* - Excludes `null`, `undefined`, and all primitive types
|
|
11
|
+
* - Excludes functions (they have `typeof` === `"function"`, not `"object"`)
|
|
12
|
+
* - Includes arrays, dates, regex, and other object instances
|
|
13
|
+
*
|
|
14
|
+
* **Note:** This function returns `true` for arrays. If you need to check for plain objects
|
|
15
|
+
* specifically (excluding arrays), use `isRecord()` instead.
|
|
16
|
+
*
|
|
17
|
+
* @param u - The value to check
|
|
18
|
+
* @returns `true` if `u` is an object and not `null`, `false` otherwise.
|
|
19
|
+
* When `true`, TypeScript narrows the type to `object`.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* Basic usage with different value types:
|
|
23
|
+
* ```typescript
|
|
24
|
+
* isNonNullObject({}); // true (plain object)
|
|
25
|
+
* isNonNullObject([]); // true (arrays are objects)
|
|
26
|
+
* isNonNullObject(new Date()); // true (Date instance)
|
|
27
|
+
* isNonNullObject(/regex/); // true (RegExp instance)
|
|
28
|
+
* isNonNullObject(new Map()); // true (Map instance)
|
|
29
|
+
* isNonNullObject(null); // false (null is not considered object here)
|
|
30
|
+
* isNonNullObject(undefined); // false (primitive)
|
|
31
|
+
* isNonNullObject("string"); // false (primitive)
|
|
32
|
+
* isNonNullObject(42); // false (primitive)
|
|
33
|
+
* isNonNullObject(true); // false (primitive)
|
|
34
|
+
* isNonNullObject(() => {}); // false (functions are not objects in this context)
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* Type guard usage with unknown values:
|
|
39
|
+
* ```typescript
|
|
40
|
+
* const value: unknown = parseJsonData();
|
|
41
|
+
*
|
|
42
|
+
* if (isNonNullObject(value)) {
|
|
43
|
+
* // value is now typed as object
|
|
44
|
+
* console.log('Value is an object');
|
|
45
|
+
*
|
|
46
|
+
* // You can now safely use object-specific operations
|
|
47
|
+
* console.log(Object.keys(value)); // Safe to call Object.keys
|
|
48
|
+
* console.log(value.toString()); // Safe to call methods
|
|
49
|
+
*
|
|
50
|
+
* // But you may need additional checks for specific object types
|
|
51
|
+
* if (Array.isArray(value)) {
|
|
52
|
+
* console.log('It\'s an array with length:', value.length);
|
|
53
|
+
* }
|
|
54
|
+
* } else {
|
|
55
|
+
* console.log('Value is not an object');
|
|
56
|
+
* }
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* Filtering arrays to find objects:
|
|
61
|
+
* ```typescript
|
|
62
|
+
* const mixedArray: unknown[] = [
|
|
63
|
+
* { name: 'John' },
|
|
64
|
+
* 'string',
|
|
65
|
+
* [1, 2, 3],
|
|
66
|
+
* 42,
|
|
67
|
+
* null,
|
|
68
|
+
* new Date(),
|
|
69
|
+
* () => 'function'
|
|
70
|
+
* ];
|
|
71
|
+
*
|
|
72
|
+
* const objects = mixedArray.filter(isNonNullObject);
|
|
73
|
+
* // objects contains: [{ name: 'John' }, [1, 2, 3], Date instance]
|
|
74
|
+
* // Note: includes both plain objects and arrays
|
|
75
|
+
*
|
|
76
|
+
* objects.forEach(obj => {
|
|
77
|
+
* // Each obj is guaranteed to be an object
|
|
78
|
+
* console.log('Object type:', obj.constructor.name);
|
|
79
|
+
* });
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* Progressive type narrowing with other guards:
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const apiResponse: unknown = await fetchData();
|
|
86
|
+
*
|
|
87
|
+
* if (isNonNullObject(apiResponse)) {
|
|
88
|
+
* // apiResponse is now object
|
|
89
|
+
*
|
|
90
|
+
* if (isRecord(apiResponse)) {
|
|
91
|
+
* // Further narrowed to UnknownRecord (plain object, not array)
|
|
92
|
+
*
|
|
93
|
+
* if (hasKey(apiResponse, 'status')) {
|
|
94
|
+
* console.log('API status:', apiResponse.status);
|
|
95
|
+
* }
|
|
96
|
+
* } else if (Array.isArray(apiResponse)) {
|
|
97
|
+
* console.log('Response is an array with length:', apiResponse.length);
|
|
98
|
+
* }
|
|
99
|
+
* }
|
|
100
|
+
* ```
|
|
101
|
+
*
|
|
102
|
+
* @see {@link isRecord} - For checking plain objects specifically (excludes arrays)
|
|
103
|
+
*/
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
105
|
+
export const isNonNullObject = (u: unknown): u is object =>
|
|
106
|
+
typeof u === 'object' && u !== null;
|