ts-fortress 3.0.1 → 3.2.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/README.md +31 -18
- package/dist/entry-point.mjs +3 -0
- package/dist/entry-point.mjs.map +1 -1
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -1
- package/dist/other-types/index.d.mts +2 -0
- package/dist/other-types/index.d.mts.map +1 -1
- package/dist/other-types/index.mjs +2 -0
- package/dist/other-types/index.mjs.map +1 -1
- package/dist/other-types/map.d.mts +7 -0
- package/dist/other-types/map.d.mts.map +1 -0
- package/dist/other-types/map.mjs +70 -0
- package/dist/other-types/map.mjs.map +1 -0
- package/dist/other-types/set.d.mts +7 -0
- package/dist/other-types/set.d.mts.map +1 -0
- package/dist/other-types/set.mjs +55 -0
- package/dist/other-types/set.mjs.map +1 -0
- package/dist/predefined/index.d.mts +1 -0
- package/dist/predefined/index.d.mts.map +1 -1
- package/dist/predefined/index.mjs +1 -0
- package/dist/predefined/index.mjs.map +1 -1
- package/dist/predefined/nullable.d.mts +3 -0
- package/dist/predefined/nullable.d.mts.map +1 -0
- package/dist/predefined/nullable.mjs +8 -0
- package/dist/predefined/nullable.mjs.map +1 -0
- package/package.json +6 -6
- package/src/other-types/index.mts +2 -0
- package/src/other-types/map.mts +99 -0
- package/src/other-types/map.test.mts +181 -0
- package/src/other-types/set.mts +74 -0
- package/src/other-types/set.test.mts +199 -0
- package/src/predefined/index.mts +1 -0
- package/src/predefined/nullable.mts +6 -0
package/README.md
CHANGED
|
@@ -7,11 +7,12 @@ TypeScript-first schema validation library with static type inference.
|
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
[](https://codecov.io/gh/noshiro-pf/ts-fortress)
|
|
9
9
|
|
|
10
|
-
**ts-fortress** is a runtime validation library similar to [io-ts](https://github.com/gcanti/io-ts) and [zod](https://
|
|
10
|
+
**ts-fortress** is a runtime validation library similar to [io-ts](https://github.com/gcanti/io-ts) and [zod/mini](https://zod.dev/packages/mini), designed to provide type-safe schema validation with excellent TypeScript integration and static type inference.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- 🔒 **Type-safe validation** - Full TypeScript support with static type inference
|
|
15
|
+
- 📖 **Readonly by default** - All constructed types are fully readonly, preventing accidental mutations and promoting immutability
|
|
15
16
|
- 🏗️ **Composable schemas** - Build complex validation schemas from simple building blocks
|
|
16
17
|
- 🔄 **Result-based error handling** - Structured error reporting with `Result<T, readonly ValidationError[]>`
|
|
17
18
|
- 🏷️ **Branded types** - Rich collection of branded number types (Int, SafeInt, PositiveInt, etc.)
|
|
@@ -210,7 +211,7 @@ export const SomeObject = t.record({
|
|
|
210
211
|
|
|
211
212
|
// ✅ Correct ts-fortress usage - enforced by TypeScript
|
|
212
213
|
export const SomeObject = t.record({
|
|
213
|
-
key1: t.
|
|
214
|
+
key1: t.literal(1), // or t.number(1) with default
|
|
214
215
|
key2: t.string(''),
|
|
215
216
|
});
|
|
216
217
|
```
|
|
@@ -633,8 +634,8 @@ const nullType = t.nullType;
|
|
|
633
634
|
const undefinedType = t.undefinedType;
|
|
634
635
|
|
|
635
636
|
// Literal types
|
|
636
|
-
const statusType = t.
|
|
637
|
-
const versionType = t.
|
|
637
|
+
const statusType = t.literal('active');
|
|
638
|
+
const versionType = t.literal(1);
|
|
638
639
|
|
|
639
640
|
// Arrays
|
|
640
641
|
const stringArrayType = t.array(t.string(''));
|
|
@@ -647,6 +648,8 @@ const coordinateType = t.tuple([t.number(0), t.number(0)]);
|
|
|
647
648
|
### Record Types
|
|
648
649
|
|
|
649
650
|
```typescript
|
|
651
|
+
import * as t from 'ts-fortress';
|
|
652
|
+
|
|
650
653
|
// Define object schemas
|
|
651
654
|
const PersonType = t.record({
|
|
652
655
|
firstName: t.string(''),
|
|
@@ -732,9 +735,11 @@ UserSchema.is({
|
|
|
732
735
|
ts-fortress provides extensive support for branded types to create domain-specific validation:
|
|
733
736
|
|
|
734
737
|
```typescript
|
|
738
|
+
import * as t from 'ts-fortress';
|
|
739
|
+
|
|
735
740
|
// Simple branded types
|
|
736
|
-
const UserId = t.simpleBrandedString('UserId', '');
|
|
737
|
-
const Weight = t.simpleBrandedNumber('Weight', 0);
|
|
741
|
+
const UserId = t.simpleBrandedString({ typeName: 'UserId', defaultValue: '' });
|
|
742
|
+
const Weight = t.simpleBrandedNumber({ typeName: 'Weight', defaultValue: 0 });
|
|
738
743
|
|
|
739
744
|
type UserId = t.TypeOf<typeof UserId>; // Brand<string, 'UserId'>
|
|
740
745
|
type Weight = t.TypeOf<typeof Weight>; // Brand<number, 'Weight'>
|
|
@@ -754,14 +759,9 @@ if (t.Result.isOk(userIdResult)) {
|
|
|
754
759
|
### Union and Intersection Types
|
|
755
760
|
|
|
756
761
|
```typescript
|
|
757
|
-
|
|
758
|
-
const StatusType = t.union([
|
|
759
|
-
t.stringLiteral('pending'),
|
|
760
|
-
t.stringLiteral('completed'),
|
|
761
|
-
t.stringLiteral('failed'),
|
|
762
|
-
]);
|
|
762
|
+
import * as t from 'ts-fortress';
|
|
763
763
|
|
|
764
|
-
//
|
|
764
|
+
// Union types
|
|
765
765
|
const IdType = t.union([t.string(''), t.number(0)]);
|
|
766
766
|
|
|
767
767
|
// Intersection types
|
|
@@ -786,6 +786,8 @@ const ExtendedUserType = t.mergeRecords([
|
|
|
786
786
|
### Enums
|
|
787
787
|
|
|
788
788
|
```typescript
|
|
789
|
+
import * as t from 'ts-fortress';
|
|
790
|
+
|
|
789
791
|
// String enums
|
|
790
792
|
const ColorEnum = t.enumType(['red', 'green', 'blue']);
|
|
791
793
|
type Color = t.TypeOf<typeof ColorEnum>; // 'red' | 'green' | 'blue'
|
|
@@ -804,6 +806,8 @@ type DiceRoll = t.TypeOf<typeof DiceRoll>; // 1 | 2 | 3 | 4 | 5 | 6
|
|
|
804
806
|
ts-fortress uses `Result<T, readonly ValidationError[]>` for structured error handling with detailed error information:
|
|
805
807
|
|
|
806
808
|
```typescript
|
|
809
|
+
import * as t from 'ts-fortress';
|
|
810
|
+
|
|
807
811
|
const UserType = t.record({
|
|
808
812
|
name: t.string(''),
|
|
809
813
|
age: t.number(0),
|
|
@@ -882,7 +886,7 @@ type ValidationError = Readonly<{
|
|
|
882
886
|
- `t.number(defaultValue)` - Number validation
|
|
883
887
|
- `t.boolean(defaultValue)` - Boolean validation
|
|
884
888
|
- `t.nullType` / `t.undefinedType` - Null/undefined validation
|
|
885
|
-
- `t.
|
|
889
|
+
- `t.literal(value)` - Literal types (string, number, or boolean)
|
|
886
890
|
|
|
887
891
|
### Collections
|
|
888
892
|
|
|
@@ -909,18 +913,27 @@ type ValidationError = Readonly<{
|
|
|
909
913
|
- `t.intersection(types, defaultType)` - Intersection type validation
|
|
910
914
|
- `t.mergeRecords(recordTypes)` - Merge multiple record types
|
|
911
915
|
|
|
912
|
-
###
|
|
916
|
+
### Refinement
|
|
913
917
|
|
|
914
|
-
- `t.
|
|
915
|
-
- `t.
|
|
918
|
+
- `t.refine({ baseType, is, defaultValue })` - Refine `baseType` by `is` function
|
|
919
|
+
- `t.simpleBrandedString({ typeName, defaultValue, is? })` - Simple string branding
|
|
920
|
+
- `t.simpleBrandedNumber({ typeName, defaultValue, is? })` - Simple number branding
|
|
916
921
|
- Number types: `t.int()`, `t.safeInt()`, `t.positiveInt()`, `t.uint16()`, etc.
|
|
917
922
|
|
|
918
923
|
### Utilities
|
|
919
924
|
|
|
920
925
|
- `t.TypeOf<T>` - Extract TypeScript type from validator
|
|
921
926
|
- `t.enumType(values)` - Enum validation
|
|
922
|
-
- `t.uintRange({ start, end, defaultValue? })` -
|
|
927
|
+
- `t.uintRange({ start, end, defaultValue? })` - Non-negative integer range validation
|
|
928
|
+
- `t.intRange({ start, end, defaultValue? })` - Integer range validation
|
|
923
929
|
- `t.unknown` - Unknown Type
|
|
930
|
+
- `t.recursion(typeName, definition)` - Define recursive type
|
|
931
|
+
|
|
932
|
+
### Pre-defined types
|
|
933
|
+
|
|
934
|
+
- `t.int8` / `t.uint8` - Int8 / Uint8
|
|
935
|
+
- `t.JsonValue` / `t.JsonPrimitive` / `t.JsonObject`
|
|
936
|
+
- `t.nullable(T)` - An alias of `t.union([T, t.undefinedType])`
|
|
924
937
|
|
|
925
938
|
## Contributing
|
|
926
939
|
|
package/dist/entry-point.mjs
CHANGED
|
@@ -30,10 +30,13 @@ export { enumType } from './enum/enum.mjs';
|
|
|
30
30
|
export { intRange } from './enum/int-range.mjs';
|
|
31
31
|
export { uintRange } from './enum/uint-range.mjs';
|
|
32
32
|
export { literal } from './other-types/literal.mjs';
|
|
33
|
+
export { MapType } from './other-types/map.mjs';
|
|
33
34
|
export { refine } from './other-types/refine.mjs';
|
|
35
|
+
export { SetType } from './other-types/set.mjs';
|
|
34
36
|
export { unknown } from './other-types/unknown.mjs';
|
|
35
37
|
export { int8 } from './predefined/int8.mjs';
|
|
36
38
|
export { JsonObject, JsonPrimitive, JsonValue } from './predefined/json.mjs';
|
|
39
|
+
export { nullable } from './predefined/nullable.mjs';
|
|
37
40
|
export { uint8 } from './predefined/uint8.mjs';
|
|
38
41
|
export { bigint, bigintLiteral } from './primitives/bigint.mjs';
|
|
39
42
|
export { boolean, booleanLiteral } from './primitives/boolean.mjs';
|
package/dist/entry-point.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"entry-point.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"entry-point.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/index.mjs
CHANGED
|
@@ -29,10 +29,13 @@ export { enumType } from './enum/enum.mjs';
|
|
|
29
29
|
export { intRange } from './enum/int-range.mjs';
|
|
30
30
|
export { uintRange } from './enum/uint-range.mjs';
|
|
31
31
|
export { literal } from './other-types/literal.mjs';
|
|
32
|
+
export { MapType } from './other-types/map.mjs';
|
|
32
33
|
export { refine } from './other-types/refine.mjs';
|
|
34
|
+
export { SetType } from './other-types/set.mjs';
|
|
33
35
|
export { unknown } from './other-types/unknown.mjs';
|
|
34
36
|
export { int8 } from './predefined/int8.mjs';
|
|
35
37
|
export { JsonObject, JsonPrimitive, JsonValue } from './predefined/json.mjs';
|
|
38
|
+
export { nullable } from './predefined/nullable.mjs';
|
|
36
39
|
export { uint8 } from './predefined/uint8.mjs';
|
|
37
40
|
export { bigint, bigintLiteral } from './primitives/bigint.mjs';
|
|
38
41
|
export { boolean, booleanLiteral } from './primitives/boolean.mjs';
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/other-types/index.mts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/other-types/index.mts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Type, type TypeOf } from '../type.mjs';
|
|
2
|
+
type MapResultType<K extends Type<unknown>, V extends Type<unknown>> = ReadonlyMap<TypeOf<K>, TypeOf<V>>;
|
|
3
|
+
export declare const MapType: <K extends Type<unknown>, V extends Type<unknown>>(keyType: K, valueType: V, options?: Readonly<{
|
|
4
|
+
typeName?: string;
|
|
5
|
+
}>) => Type<MapResultType<K, V>>;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=map.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map.d.mts","sourceRoot":"","sources":["../../src/other-types/map.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAWrD,KAAK,aAAa,CAChB,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EACvB,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IACrB,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAEtC,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EACtE,SAAS,CAAC,EACV,WAAW,CAAC,EACZ,UAAU,QAAQ,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,KACxC,IAAI,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,CAAC,CA6E1B,CAAC"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Result, Arr, unknownToString } from 'ts-data-forge';
|
|
2
|
+
import { createAssertFn } from '../utils/create-assert-fn.mjs';
|
|
3
|
+
import { createCastFn } from '../utils/create-cast-fn.mjs';
|
|
4
|
+
import { createIsFn } from '../utils/create-is-fn.mjs';
|
|
5
|
+
import { createPrimitiveValidationError, prependPathToValidationErrors } from '../utils/validation-error.mjs';
|
|
6
|
+
|
|
7
|
+
const MapType = (keyType, valueType, options) => {
|
|
8
|
+
const typeName = options?.typeName ?? 'Map';
|
|
9
|
+
const defaultValue = new Map();
|
|
10
|
+
const validate = (a) => {
|
|
11
|
+
if (!(a instanceof Map)) {
|
|
12
|
+
return Result.err([
|
|
13
|
+
createPrimitiveValidationError({
|
|
14
|
+
actualValue: a,
|
|
15
|
+
expectedType: 'Map',
|
|
16
|
+
typeName,
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
const errors = Arr.generate(function* () {
|
|
21
|
+
for (const [k, v] of a.entries()) {
|
|
22
|
+
{
|
|
23
|
+
const res = keyType.validate(k);
|
|
24
|
+
if (Result.isErr(res)) {
|
|
25
|
+
yield {
|
|
26
|
+
path: [],
|
|
27
|
+
actualValue: k,
|
|
28
|
+
expectedType: typeName,
|
|
29
|
+
message: `The key of the Map is expected to be <${keyType.typeName}>, but got <${typeof k}> type value \`${unknownToString(k)}\``,
|
|
30
|
+
typeName,
|
|
31
|
+
};
|
|
32
|
+
yield* res.value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
{
|
|
36
|
+
const res = valueType.validate(v);
|
|
37
|
+
if (Result.isErr(res)) {
|
|
38
|
+
yield {
|
|
39
|
+
path: [],
|
|
40
|
+
actualValue: v,
|
|
41
|
+
expectedType: typeName,
|
|
42
|
+
message: `The value of the Map is expected to be <${valueType.typeName}>, but got <${typeof v}> type value \`${unknownToString(v)}\``,
|
|
43
|
+
typeName,
|
|
44
|
+
};
|
|
45
|
+
yield* prependPathToValidationErrors(res.value, unknownToString(k));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (errors.length > 0) {
|
|
51
|
+
return Result.err(errors);
|
|
52
|
+
}
|
|
53
|
+
return Result.ok(a);
|
|
54
|
+
};
|
|
55
|
+
const fill = (a) => a instanceof Map
|
|
56
|
+
? new Map(Array.from(a.entries()).filter(([k, v]) => keyType.is(k) && valueType.is(v)))
|
|
57
|
+
: defaultValue;
|
|
58
|
+
return {
|
|
59
|
+
typeName,
|
|
60
|
+
defaultValue,
|
|
61
|
+
fill,
|
|
62
|
+
validate,
|
|
63
|
+
is: createIsFn(validate),
|
|
64
|
+
assertIs: createAssertFn(validate),
|
|
65
|
+
cast: createCastFn(validate),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export { MapType };
|
|
70
|
+
//# sourceMappingURL=map.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"map.mjs","sources":["../../src/other-types/map.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;AAiBO,MAAM,OAAO,GAAG,CACrB,OAAU,EACV,SAAY,EACZ,OAAyC,KACZ;AAG7B,IAAA,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,KAAK;AAE3C,IAAA,MAAM,YAAY,GAAM,IAAI,GAAG,EAAE;AAEjC,IAAA,MAAM,QAAQ,GAAwB,CAAC,CAAC,KAAI;AAC1C,QAAA,IAAI,EAAE,CAAC,YAAY,GAAG,CAAC,EAAE;YACvB,OAAO,MAAM,CAAC,GAAG,CAAC;AAChB,gBAAA,8BAA8B,CAAC;AAC7B,oBAAA,WAAW,EAAE,CAAC;AACd,oBAAA,YAAY,EAAE,KAAK;oBACnB,QAAQ;iBACT,CAAC;AACH,aAAA,CAAC;QACJ;AAEA,QAAA,MAAM,MAAM,GAA+B,GAAG,CAAC,QAAQ,CAAC,aAAS;AAC/D,YAAA,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAK,CAAmC,CAAC,OAAO,EAAE,EAAE;gBACnE;oBACE,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC;AAE/B,oBAAA,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;wBACrB,MAAM;AACJ,4BAAA,IAAI,EAAE,EAAE;AACR,4BAAA,WAAW,EAAE,CAAC;AACd,4BAAA,YAAY,EAAE,QAAQ;AACtB,4BAAA,OAAO,EAAE,CAAA,sCAAA,EAAyC,OAAO,CAAC,QAAQ,CAAA,YAAA,EAAe,OAAO,CAAC,CAAA,eAAA,EAAkB,eAAe,CAAC,CAAC,CAAC,CAAA,EAAA,CAAI;4BACjI,QAAQ;yBAC4B;AAEtC,wBAAA,OAAO,GAAG,CAAC,KAAK;oBAClB;gBACF;gBACA;oBACE,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;AACjC,oBAAA,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;wBACrB,MAAM;AACJ,4BAAA,IAAI,EAAE,EAAE;AACR,4BAAA,WAAW,EAAE,CAAC;AACd,4BAAA,YAAY,EAAE,QAAQ;AACtB,4BAAA,OAAO,EAAE,CAAA,wCAAA,EAA2C,SAAS,CAAC,QAAQ,CAAA,YAAA,EAAe,OAAO,CAAC,CAAA,eAAA,EAAkB,eAAe,CAAC,CAAC,CAAC,CAAA,EAAA,CAAI;4BACrI,QAAQ;yBAC4B;AAEtC,wBAAA,OAAO,6BAA6B,CAAC,GAAG,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC;oBACrE;gBACF;YACF;AACF,QAAA,CAAC,CAAC;AAEF,QAAA,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE;AACrB,YAAA,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QAC3B;AAEA,QAAA,OAAO,MAAM,CAAC,EAAE,CAAC,CAAM,CAAC;AAC1B,IAAA,CAAC;IAED,MAAM,IAAI,GAAoB,CAAC,CAAC,KAC9B,CAAC,YAAY;AACX,UAAG,IAAI,GAAG,CACN,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAC5B,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,EAAE,CAAC,CAAC,CAAC,CAC7C;UAEH,YAAY;IAElB,OAAO;QACL,QAAQ;QACR,YAAY;QACZ,IAAI;QACJ,QAAQ;AACR,QAAA,EAAE,EAAE,UAAU,CAAC,QAAQ,CAAC;AACxB,QAAA,QAAQ,EAAE,cAAc,CAAC,QAAQ,CAAC;AAClC,QAAA,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC;KAC7B;AACH;;;;"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type Type, type TypeOf } from '../type.mjs';
|
|
2
|
+
type SetResultType<T extends Type<unknown>> = ReadonlySet<TypeOf<T>>;
|
|
3
|
+
export declare const SetType: <T extends Type<unknown>>(elementType: T, options?: Readonly<{
|
|
4
|
+
typeName?: string;
|
|
5
|
+
}>) => Type<SetResultType<T>>;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=set.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set.d.mts","sourceRoot":"","sources":["../../src/other-types/set.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAUrD,KAAK,aAAa,CAAC,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAErE,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EAC7C,aAAa,CAAC,EACd,UAAU,QAAQ,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,KACxC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAyDvB,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Result, Arr, unknownToString } from 'ts-data-forge';
|
|
2
|
+
import { createAssertFn } from '../utils/create-assert-fn.mjs';
|
|
3
|
+
import { createCastFn } from '../utils/create-cast-fn.mjs';
|
|
4
|
+
import { createIsFn } from '../utils/create-is-fn.mjs';
|
|
5
|
+
import { createPrimitiveValidationError } from '../utils/validation-error.mjs';
|
|
6
|
+
|
|
7
|
+
const SetType = (elementType, options) => {
|
|
8
|
+
const typeName = options?.typeName ?? 'Set';
|
|
9
|
+
const defaultValue = new Set();
|
|
10
|
+
const validate = (a) => {
|
|
11
|
+
if (!(a instanceof Set)) {
|
|
12
|
+
return Result.err([
|
|
13
|
+
createPrimitiveValidationError({
|
|
14
|
+
actualValue: a,
|
|
15
|
+
expectedType: 'Set',
|
|
16
|
+
typeName,
|
|
17
|
+
}),
|
|
18
|
+
]);
|
|
19
|
+
}
|
|
20
|
+
const errors = Arr.generate(function* () {
|
|
21
|
+
for (const element of a.values()) {
|
|
22
|
+
const res = elementType.validate(element);
|
|
23
|
+
if (Result.isErr(res)) {
|
|
24
|
+
yield {
|
|
25
|
+
path: [],
|
|
26
|
+
actualValue: element,
|
|
27
|
+
expectedType: typeName,
|
|
28
|
+
message: `The element of the Set is expected to be <${elementType.typeName}>, but got <${typeof element}> type value \`${unknownToString(element)}\``,
|
|
29
|
+
typeName,
|
|
30
|
+
};
|
|
31
|
+
yield* res.value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
if (errors.length > 0) {
|
|
36
|
+
return Result.err(errors);
|
|
37
|
+
}
|
|
38
|
+
return Result.ok(a);
|
|
39
|
+
};
|
|
40
|
+
const fill = (a) => a instanceof Set
|
|
41
|
+
? new Set(Array.from(a.values()).filter((v) => elementType.is(v)))
|
|
42
|
+
: defaultValue;
|
|
43
|
+
return {
|
|
44
|
+
typeName,
|
|
45
|
+
defaultValue,
|
|
46
|
+
fill,
|
|
47
|
+
validate,
|
|
48
|
+
is: createIsFn(validate),
|
|
49
|
+
assertIs: createAssertFn(validate),
|
|
50
|
+
cast: createCastFn(validate),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export { SetType };
|
|
55
|
+
//# sourceMappingURL=set.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"set.mjs","sources":["../../src/other-types/set.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;;MAaa,OAAO,GAAG,CACrB,WAAc,EACd,OAAyC,KACf;AAG1B,IAAA,MAAM,QAAQ,GAAG,OAAO,EAAE,QAAQ,IAAI,KAAK;AAE3C,IAAA,MAAM,YAAY,GAAM,IAAI,GAAG,EAAE;AAEjC,IAAA,MAAM,QAAQ,GAAwB,CAAC,CAAC,KAAI;AAC1C,QAAA,IAAI,EAAE,CAAC,YAAY,GAAG,CAAC,EAAE;YACvB,OAAO,MAAM,CAAC,GAAG,CAAC;AAChB,gBAAA,8BAA8B,CAAC;AAC7B,oBAAA,WAAW,EAAE,CAAC;AACd,oBAAA,YAAY,EAAE,KAAK;oBACnB,QAAQ;iBACT,CAAC;AACH,aAAA,CAAC;QACJ;AAEA,QAAA,MAAM,MAAM,GAA+B,GAAG,CAAC,QAAQ,CAAC,aAAS;YAC/D,KAAK,MAAM,OAAO,IAAK,CAA0B,CAAC,MAAM,EAAE,EAAE;gBAC1D,MAAM,GAAG,GAAG,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC;AAEzC,gBAAA,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;oBACrB,MAAM;AACJ,wBAAA,IAAI,EAAE,EAAE;AACR,wBAAA,WAAW,EAAE,OAAO;AACpB,wBAAA,YAAY,EAAE,QAAQ;AACtB,wBAAA,OAAO,EAAE,CAAA,0CAAA,EAA6C,WAAW,CAAC,QAAQ,CAAA,YAAA,EAAe,OAAO,OAAO,CAAA,eAAA,EAAkB,eAAe,CAAC,OAAO,CAAC,CAAA,EAAA,CAAI;wBACrJ,QAAQ;qBAC4B;AAEtC,oBAAA,OAAO,GAAG,CAAC,KAAK;gBAClB;YACF;AACF,QAAA,CAAC,CAAC;AAEF,QAAA,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE;AACrB,YAAA,OAAO,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;QAC3B;AAEA,QAAA,OAAO,MAAM,CAAC,EAAE,CAAC,CAAM,CAAC;AAC1B,IAAA,CAAC;IAED,MAAM,IAAI,GAAoB,CAAC,CAAC,KAC9B,CAAC,YAAY;AACX,UAAG,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;UAChE,YAAY;IAElB,OAAO;QACL,QAAQ;QACR,YAAY;QACZ,IAAI;QACJ,QAAQ;AACR,QAAA,EAAE,EAAE,UAAU,CAAC,QAAQ,CAAC;AACxB,QAAA,QAAQ,EAAE,cAAc,CAAC,QAAQ,CAAC;AAClC,QAAA,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC;KAC7B;AACH;;;;"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/predefined/index.mts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/predefined/index.mts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,aAAa,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nullable.d.mts","sourceRoot":"","sources":["../../src/predefined/nullable.mts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAG,MAAM,IAAI,CAAC,CAAC,CAAC,KAAG,IAAI,CAAC,CAAC,GAAG,SAAS,CACjC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nullable.mjs","sources":["../../src/predefined/nullable.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;AAIO,MAAM,QAAQ,GAAG,CAAK,IAAa,KACxC,KAAK,CAAC,CAAC,IAAI,EAAE,aAAa,CAAC;;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-fortress",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript"
|
|
@@ -65,14 +65,14 @@
|
|
|
65
65
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
66
66
|
"@semantic-release/exec": "^7.1.0",
|
|
67
67
|
"@semantic-release/git": "^10.0.1",
|
|
68
|
-
"@semantic-release/github": "^11.0.
|
|
68
|
+
"@semantic-release/github": "^11.0.5",
|
|
69
69
|
"@semantic-release/npm": "^12.0.2",
|
|
70
70
|
"@semantic-release/release-notes-generator": "^14.0.3",
|
|
71
|
-
"@types/node": "^24.3.
|
|
71
|
+
"@types/node": "^24.3.1",
|
|
72
72
|
"@vitest/coverage-v8": "^3.2.4",
|
|
73
73
|
"@vitest/ui": "^3.2.4",
|
|
74
74
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
75
|
-
"cspell": "^9.2.
|
|
75
|
+
"cspell": "^9.2.1",
|
|
76
76
|
"eslint": "^9.34.0",
|
|
77
77
|
"eslint-import-resolver-typescript": "4.4.4",
|
|
78
78
|
"eslint-plugin-array-func": "5.0.2",
|
|
@@ -89,12 +89,12 @@
|
|
|
89
89
|
"prettier": "^3.6.2",
|
|
90
90
|
"prettier-plugin-organize-imports": "^4.2.0",
|
|
91
91
|
"prettier-plugin-packagejson": "^2.5.19",
|
|
92
|
-
"rollup": "^4.
|
|
92
|
+
"rollup": "^4.50.0",
|
|
93
93
|
"semantic-release": "^24.2.7",
|
|
94
94
|
"ts-repo-utils": "^7.0.4",
|
|
95
95
|
"tslib": "^2.8.1",
|
|
96
96
|
"tsx": "^4.20.4",
|
|
97
|
-
"typedoc": "^0.28.
|
|
97
|
+
"typedoc": "^0.28.12",
|
|
98
98
|
"typedoc-plugin-markdown": "^4.8.1",
|
|
99
99
|
"typescript": "^5.9.2",
|
|
100
100
|
"typescript-eslint": "^8.41.0",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Arr, Result, unknownToString } from 'ts-data-forge';
|
|
2
|
+
import { type Type, type TypeOf } from '../type.mjs';
|
|
3
|
+
import {
|
|
4
|
+
createAssertFn,
|
|
5
|
+
createCastFn,
|
|
6
|
+
createIsFn,
|
|
7
|
+
createPrimitiveValidationError,
|
|
8
|
+
prependPathToValidationErrors,
|
|
9
|
+
type ValidationError,
|
|
10
|
+
type ValidationErrorWithMessage,
|
|
11
|
+
} from '../utils/index.mjs';
|
|
12
|
+
|
|
13
|
+
type MapResultType<
|
|
14
|
+
K extends Type<unknown>,
|
|
15
|
+
V extends Type<unknown>,
|
|
16
|
+
> = ReadonlyMap<TypeOf<K>, TypeOf<V>>;
|
|
17
|
+
|
|
18
|
+
export const MapType = <K extends Type<unknown>, V extends Type<unknown>>(
|
|
19
|
+
keyType: K,
|
|
20
|
+
valueType: V,
|
|
21
|
+
options?: Readonly<{ typeName?: string }>,
|
|
22
|
+
): Type<MapResultType<K, V>> => {
|
|
23
|
+
type M = MapResultType<K, V>;
|
|
24
|
+
|
|
25
|
+
const typeName = options?.typeName ?? 'Map';
|
|
26
|
+
|
|
27
|
+
const defaultValue: M = new Map();
|
|
28
|
+
|
|
29
|
+
const validate: Type<M>['validate'] = (a) => {
|
|
30
|
+
if (!(a instanceof Map)) {
|
|
31
|
+
return Result.err([
|
|
32
|
+
createPrimitiveValidationError({
|
|
33
|
+
actualValue: a,
|
|
34
|
+
expectedType: 'Map',
|
|
35
|
+
typeName,
|
|
36
|
+
}),
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const errors: readonly ValidationError[] = Arr.generate(function* () {
|
|
41
|
+
for (const [k, v] of (a as ReadonlyMap<unknown, unknown>).entries()) {
|
|
42
|
+
{
|
|
43
|
+
const res = keyType.validate(k);
|
|
44
|
+
|
|
45
|
+
if (Result.isErr(res)) {
|
|
46
|
+
yield {
|
|
47
|
+
path: [],
|
|
48
|
+
actualValue: k,
|
|
49
|
+
expectedType: typeName,
|
|
50
|
+
message: `The key of the Map is expected to be <${keyType.typeName}>, but got <${typeof k}> type value \`${unknownToString(k)}\``,
|
|
51
|
+
typeName,
|
|
52
|
+
} satisfies ValidationErrorWithMessage;
|
|
53
|
+
|
|
54
|
+
yield* res.value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
{
|
|
58
|
+
const res = valueType.validate(v);
|
|
59
|
+
if (Result.isErr(res)) {
|
|
60
|
+
yield {
|
|
61
|
+
path: [],
|
|
62
|
+
actualValue: v,
|
|
63
|
+
expectedType: typeName,
|
|
64
|
+
message: `The value of the Map is expected to be <${valueType.typeName}>, but got <${typeof v}> type value \`${unknownToString(v)}\``,
|
|
65
|
+
typeName,
|
|
66
|
+
} satisfies ValidationErrorWithMessage;
|
|
67
|
+
|
|
68
|
+
yield* prependPathToValidationErrors(res.value, unknownToString(k));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (errors.length > 0) {
|
|
75
|
+
return Result.err(errors);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Result.ok(a as M);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const fill: Type<M>['fill'] = (a) =>
|
|
82
|
+
a instanceof Map
|
|
83
|
+
? (new Map(
|
|
84
|
+
Array.from(a.entries()).filter(
|
|
85
|
+
([k, v]) => keyType.is(k) && valueType.is(v),
|
|
86
|
+
),
|
|
87
|
+
) as M)
|
|
88
|
+
: defaultValue;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
typeName,
|
|
92
|
+
defaultValue,
|
|
93
|
+
fill,
|
|
94
|
+
validate,
|
|
95
|
+
is: createIsFn(validate),
|
|
96
|
+
assertIs: createAssertFn(validate),
|
|
97
|
+
cast: createCastFn(validate),
|
|
98
|
+
};
|
|
99
|
+
};
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { expectType, Result } from 'ts-data-forge';
|
|
2
|
+
import { number, string } from '../primitives/index.mjs';
|
|
3
|
+
import { type TypeOf } from '../type.mjs';
|
|
4
|
+
import { MapType } from './map.mjs';
|
|
5
|
+
|
|
6
|
+
test('MapType with string keys and number values', () => {
|
|
7
|
+
const StringNumberMap = MapType(string(''), number(0));
|
|
8
|
+
type StringNumberMap = TypeOf<typeof StringNumberMap>;
|
|
9
|
+
|
|
10
|
+
// Type test
|
|
11
|
+
expectType<StringNumberMap, ReadonlyMap<string, number>>('=');
|
|
12
|
+
|
|
13
|
+
// Valid map
|
|
14
|
+
const validMap = new Map([
|
|
15
|
+
['a', 1],
|
|
16
|
+
['b', 2],
|
|
17
|
+
['c', 3],
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
// is() test
|
|
21
|
+
expect(StringNumberMap.is(validMap)).toBe(true);
|
|
22
|
+
expect(StringNumberMap.is(new Map())).toBe(true);
|
|
23
|
+
expect(StringNumberMap.is({})).toBe(false);
|
|
24
|
+
expect(StringNumberMap.is([])).toBe(false);
|
|
25
|
+
expect(StringNumberMap.is(null)).toBe(false);
|
|
26
|
+
expect(StringNumberMap.is(undefined)).toBe(false);
|
|
27
|
+
|
|
28
|
+
// validate() test
|
|
29
|
+
const result = StringNumberMap.validate(validMap);
|
|
30
|
+
expect(Result.isOk(result)).toBe(true);
|
|
31
|
+
if (Result.isOk(result)) {
|
|
32
|
+
expect(result.value).toBe(validMap); // Same reference
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('MapType with invalid key types', () => {
|
|
37
|
+
const StringNumberMap = MapType(string(''), number(0));
|
|
38
|
+
|
|
39
|
+
const invalidMap = new Map<unknown, unknown>([
|
|
40
|
+
['valid', 1],
|
|
41
|
+
[123, 2], // Invalid key type
|
|
42
|
+
['another', 3],
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// is() test
|
|
46
|
+
expect(StringNumberMap.is(invalidMap)).toBe(false);
|
|
47
|
+
|
|
48
|
+
// validate() test
|
|
49
|
+
const result = StringNumberMap.validate(invalidMap);
|
|
50
|
+
expect(Result.isErr(result)).toBe(true);
|
|
51
|
+
if (Result.isErr(result)) {
|
|
52
|
+
expect(result.value.length).toBeGreaterThan(0);
|
|
53
|
+
expect(result.value[0]?.message).toContain('key of the Map');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('MapType with invalid value types', () => {
|
|
58
|
+
const StringNumberMap = MapType(string(''), number(0));
|
|
59
|
+
|
|
60
|
+
const invalidMap = new Map<unknown, unknown>([
|
|
61
|
+
['a', 1],
|
|
62
|
+
['b', 'not a number'], // Invalid value type
|
|
63
|
+
['c', 3],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// is() test
|
|
67
|
+
expect(StringNumberMap.is(invalidMap)).toBe(false);
|
|
68
|
+
|
|
69
|
+
// validate() test
|
|
70
|
+
const result = StringNumberMap.validate(invalidMap);
|
|
71
|
+
expect(Result.isErr(result)).toBe(true);
|
|
72
|
+
if (Result.isErr(result)) {
|
|
73
|
+
expect(result.value.length).toBeGreaterThan(0);
|
|
74
|
+
expect(result.value[0]?.message).toContain('value of the Map');
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('MapType fill() method', () => {
|
|
79
|
+
const StringNumberMap = MapType(string(''), number(0));
|
|
80
|
+
|
|
81
|
+
// Valid map - should preserve valid entries
|
|
82
|
+
const validMap = new Map([
|
|
83
|
+
['a', 1],
|
|
84
|
+
['b', 2],
|
|
85
|
+
]);
|
|
86
|
+
const filled1 = StringNumberMap.fill(validMap);
|
|
87
|
+
expect(filled1.size).toBe(2);
|
|
88
|
+
expect(filled1.get('a')).toBe(1);
|
|
89
|
+
expect(filled1.get('b')).toBe(2);
|
|
90
|
+
|
|
91
|
+
// Mixed valid/invalid entries - should filter out invalid ones
|
|
92
|
+
const mixedMap = new Map<unknown, unknown>([
|
|
93
|
+
['valid', 1],
|
|
94
|
+
[123, 2], // Invalid key
|
|
95
|
+
['another', 'not a number'], // Invalid value
|
|
96
|
+
['good', 3],
|
|
97
|
+
]);
|
|
98
|
+
const filled2 = StringNumberMap.fill(mixedMap);
|
|
99
|
+
expect(filled2.size).toBe(2);
|
|
100
|
+
expect(filled2.get('valid')).toBe(1);
|
|
101
|
+
expect(filled2.get('good')).toBe(3);
|
|
102
|
+
|
|
103
|
+
// Non-map input - should return default (empty map)
|
|
104
|
+
const filled3 = StringNumberMap.fill({});
|
|
105
|
+
expect(filled3.size).toBe(0);
|
|
106
|
+
expect(filled3).toStrictEqual(new Map());
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('MapType cast() method', () => {
|
|
110
|
+
const StringNumberMap = MapType(string(''), number(0));
|
|
111
|
+
|
|
112
|
+
// Valid map
|
|
113
|
+
const validMap = new Map([['key', 42]]);
|
|
114
|
+
const casted = StringNumberMap.cast(validMap);
|
|
115
|
+
expect(casted).toBe(validMap);
|
|
116
|
+
|
|
117
|
+
// Invalid input - should throw
|
|
118
|
+
expect(() => StringNumberMap.cast('not a map')).toThrow(
|
|
119
|
+
'Expected <Map>, got <string> type value',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('MapType assertIs() method', () => {
|
|
124
|
+
const StringNumberMap = MapType(string(''), number(0));
|
|
125
|
+
|
|
126
|
+
const assertIsStringNumberMap: (
|
|
127
|
+
a: unknown,
|
|
128
|
+
) => asserts a is ReadonlyMap<string, number> = StringNumberMap.assertIs;
|
|
129
|
+
|
|
130
|
+
// Valid map - should not throw
|
|
131
|
+
const validMap = new Map([['key', 42]]);
|
|
132
|
+
expect(() => {
|
|
133
|
+
assertIsStringNumberMap(validMap);
|
|
134
|
+
}).not.toThrow();
|
|
135
|
+
|
|
136
|
+
// Invalid input - should throw
|
|
137
|
+
expect(() => {
|
|
138
|
+
assertIsStringNumberMap('not a map');
|
|
139
|
+
}).toThrow('Expected <Map>, got <string> type value');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('MapType with custom typeName', () => {
|
|
143
|
+
const CustomMap = MapType(string(''), number(0), {
|
|
144
|
+
typeName: 'CustomStringNumberMap',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(CustomMap.typeName).toBe('CustomStringNumberMap');
|
|
148
|
+
|
|
149
|
+
// Error message should include custom type name
|
|
150
|
+
const result = CustomMap.validate('not a map');
|
|
151
|
+
expect(Result.isErr(result)).toBe(true);
|
|
152
|
+
if (Result.isErr(result)) {
|
|
153
|
+
expect(result.value[0]?.typeName).toBe('CustomStringNumberMap');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('MapType with number keys and string values', () => {
|
|
158
|
+
const NumberStringMap = MapType(number(0), string(''));
|
|
159
|
+
type NumberStringMap = TypeOf<typeof NumberStringMap>;
|
|
160
|
+
|
|
161
|
+
// Type test
|
|
162
|
+
expectType<NumberStringMap, ReadonlyMap<number, string>>('=');
|
|
163
|
+
|
|
164
|
+
const validMap = new Map([
|
|
165
|
+
[1, 'one'],
|
|
166
|
+
[2, 'two'],
|
|
167
|
+
[3, 'three'],
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
expect(NumberStringMap.is(validMap)).toBe(true);
|
|
171
|
+
|
|
172
|
+
const result = NumberStringMap.validate(validMap);
|
|
173
|
+
expect(Result.isOk(result)).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('MapType defaultValue', () => {
|
|
177
|
+
const StringNumberMap = MapType(string(''), number(0));
|
|
178
|
+
|
|
179
|
+
expect(StringNumberMap.defaultValue).toStrictEqual(new Map());
|
|
180
|
+
expect(StringNumberMap.defaultValue.size).toBe(0);
|
|
181
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { Arr, Result, unknownToString } from 'ts-data-forge';
|
|
2
|
+
import { type Type, type TypeOf } from '../type.mjs';
|
|
3
|
+
import {
|
|
4
|
+
createAssertFn,
|
|
5
|
+
createCastFn,
|
|
6
|
+
createIsFn,
|
|
7
|
+
createPrimitiveValidationError,
|
|
8
|
+
type ValidationError,
|
|
9
|
+
type ValidationErrorWithMessage,
|
|
10
|
+
} from '../utils/index.mjs';
|
|
11
|
+
|
|
12
|
+
type SetResultType<T extends Type<unknown>> = ReadonlySet<TypeOf<T>>;
|
|
13
|
+
|
|
14
|
+
export const SetType = <T extends Type<unknown>>(
|
|
15
|
+
elementType: T,
|
|
16
|
+
options?: Readonly<{ typeName?: string }>,
|
|
17
|
+
): Type<SetResultType<T>> => {
|
|
18
|
+
type S = SetResultType<T>;
|
|
19
|
+
|
|
20
|
+
const typeName = options?.typeName ?? 'Set';
|
|
21
|
+
|
|
22
|
+
const defaultValue: S = new Set();
|
|
23
|
+
|
|
24
|
+
const validate: Type<S>['validate'] = (a) => {
|
|
25
|
+
if (!(a instanceof Set)) {
|
|
26
|
+
return Result.err([
|
|
27
|
+
createPrimitiveValidationError({
|
|
28
|
+
actualValue: a,
|
|
29
|
+
expectedType: 'Set',
|
|
30
|
+
typeName,
|
|
31
|
+
}),
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const errors: readonly ValidationError[] = Arr.generate(function* () {
|
|
36
|
+
for (const element of (a as ReadonlySet<unknown>).values()) {
|
|
37
|
+
const res = elementType.validate(element);
|
|
38
|
+
|
|
39
|
+
if (Result.isErr(res)) {
|
|
40
|
+
yield {
|
|
41
|
+
path: [],
|
|
42
|
+
actualValue: element,
|
|
43
|
+
expectedType: typeName,
|
|
44
|
+
message: `The element of the Set is expected to be <${elementType.typeName}>, but got <${typeof element}> type value \`${unknownToString(element)}\``,
|
|
45
|
+
typeName,
|
|
46
|
+
} satisfies ValidationErrorWithMessage;
|
|
47
|
+
|
|
48
|
+
yield* res.value;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (errors.length > 0) {
|
|
54
|
+
return Result.err(errors);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return Result.ok(a as S);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const fill: Type<S>['fill'] = (a) =>
|
|
61
|
+
a instanceof Set
|
|
62
|
+
? (new Set(Array.from(a.values()).filter((v) => elementType.is(v))) as S)
|
|
63
|
+
: defaultValue;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
typeName,
|
|
67
|
+
defaultValue,
|
|
68
|
+
fill,
|
|
69
|
+
validate,
|
|
70
|
+
is: createIsFn(validate),
|
|
71
|
+
assertIs: createAssertFn(validate),
|
|
72
|
+
cast: createCastFn(validate),
|
|
73
|
+
};
|
|
74
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { expectType, Result } from 'ts-data-forge';
|
|
2
|
+
import { test } from 'vitest';
|
|
3
|
+
import { number, string } from '../primitives/index.mjs';
|
|
4
|
+
import { record } from '../record/index.mjs';
|
|
5
|
+
import { type TypeOf } from '../type.mjs';
|
|
6
|
+
import { SetType } from './set.mjs';
|
|
7
|
+
|
|
8
|
+
test('SetType with string elements', () => {
|
|
9
|
+
const StringSet = SetType(string(''));
|
|
10
|
+
type StringSet = TypeOf<typeof StringSet>;
|
|
11
|
+
|
|
12
|
+
// Type test
|
|
13
|
+
expectType<StringSet, ReadonlySet<string>>('=');
|
|
14
|
+
|
|
15
|
+
// Valid set
|
|
16
|
+
const validSet = new Set(['a', 'b', 'c']);
|
|
17
|
+
|
|
18
|
+
// is() test
|
|
19
|
+
expect(StringSet.is(validSet)).toBe(true);
|
|
20
|
+
expect(StringSet.is(new Set())).toBe(true);
|
|
21
|
+
expect(StringSet.is({})).toBe(false);
|
|
22
|
+
expect(StringSet.is([])).toBe(false);
|
|
23
|
+
expect(StringSet.is(null)).toBe(false);
|
|
24
|
+
expect(StringSet.is(undefined)).toBe(false);
|
|
25
|
+
|
|
26
|
+
// validate() test
|
|
27
|
+
const result = StringSet.validate(validSet);
|
|
28
|
+
expect(Result.isOk(result)).toBe(true);
|
|
29
|
+
if (Result.isOk(result)) {
|
|
30
|
+
expect(result.value).toBe(validSet); // Same reference
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('SetType with number elements', () => {
|
|
35
|
+
const NumberSet = SetType(number(0));
|
|
36
|
+
type NumberSet = TypeOf<typeof NumberSet>;
|
|
37
|
+
|
|
38
|
+
// Type test
|
|
39
|
+
expectType<NumberSet, ReadonlySet<number>>('=');
|
|
40
|
+
|
|
41
|
+
const validSet = new Set([1, 2, 3, 4, 5]);
|
|
42
|
+
|
|
43
|
+
// is() test
|
|
44
|
+
expect(NumberSet.is(validSet)).toBe(true);
|
|
45
|
+
|
|
46
|
+
// validate() test
|
|
47
|
+
const result = NumberSet.validate(validSet);
|
|
48
|
+
expect(Result.isOk(result)).toBe(true);
|
|
49
|
+
if (Result.isOk(result)) {
|
|
50
|
+
expect(result.value).toBe(validSet);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('SetType with invalid element types', () => {
|
|
55
|
+
const StringSet = SetType(string(''));
|
|
56
|
+
|
|
57
|
+
const invalidSet = new Set<unknown>(['valid', 123, 'another', true]);
|
|
58
|
+
|
|
59
|
+
// is() test
|
|
60
|
+
expect(StringSet.is(invalidSet)).toBe(false);
|
|
61
|
+
|
|
62
|
+
// validate() test
|
|
63
|
+
const result = StringSet.validate(invalidSet);
|
|
64
|
+
expect(Result.isErr(result)).toBe(true);
|
|
65
|
+
if (Result.isErr(result)) {
|
|
66
|
+
expect(result.value.length).toBeGreaterThan(0);
|
|
67
|
+
expect(result.value[0]?.message).toContain('element of the Set');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('SetType fill() method', () => {
|
|
72
|
+
const NumberSet = SetType(number(0));
|
|
73
|
+
|
|
74
|
+
// Valid set - should preserve all elements
|
|
75
|
+
const validSet = new Set([1, 2, 3]);
|
|
76
|
+
const filled1 = NumberSet.fill(validSet);
|
|
77
|
+
expect(filled1.size).toBe(3);
|
|
78
|
+
expect(filled1.has(1)).toBe(true);
|
|
79
|
+
expect(filled1.has(2)).toBe(true);
|
|
80
|
+
expect(filled1.has(3)).toBe(true);
|
|
81
|
+
|
|
82
|
+
// Mixed valid/invalid elements - should filter out invalid ones
|
|
83
|
+
const mixedSet = new Set<unknown>([1, 'not a number', 2, true, 3]);
|
|
84
|
+
const filled2 = NumberSet.fill(mixedSet);
|
|
85
|
+
expect(filled2.size).toBe(3);
|
|
86
|
+
expect(filled2.has(1)).toBe(true);
|
|
87
|
+
expect(filled2.has(2)).toBe(true);
|
|
88
|
+
expect(filled2.has(3)).toBe(true);
|
|
89
|
+
|
|
90
|
+
// Non-set input - should return default (empty set)
|
|
91
|
+
const filled3 = NumberSet.fill({});
|
|
92
|
+
expect(filled3.size).toBe(0);
|
|
93
|
+
expect(filled3).toStrictEqual(new Set());
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('SetType cast() method', () => {
|
|
97
|
+
const StringSet = SetType(string(''));
|
|
98
|
+
|
|
99
|
+
// Valid set
|
|
100
|
+
const validSet = new Set(['hello', 'world']);
|
|
101
|
+
const casted = StringSet.cast(validSet);
|
|
102
|
+
expect(casted).toBe(validSet);
|
|
103
|
+
|
|
104
|
+
// Invalid input - should throw
|
|
105
|
+
expect(() => StringSet.cast('not a set')).toThrow(
|
|
106
|
+
'Expected <Set>, got <string> type value',
|
|
107
|
+
);
|
|
108
|
+
expect(() => StringSet.cast([])).toThrow(
|
|
109
|
+
'Expected <Set>, got <object> type value',
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('SetType assertIs() method', () => {
|
|
114
|
+
const NumberSet = SetType(number(0));
|
|
115
|
+
|
|
116
|
+
const assertIsNumberSet: (a: unknown) => asserts a is ReadonlySet<number> =
|
|
117
|
+
NumberSet.assertIs;
|
|
118
|
+
|
|
119
|
+
// Valid set - should not throw
|
|
120
|
+
const validSet = new Set([1, 2, 3]);
|
|
121
|
+
expect(() => {
|
|
122
|
+
assertIsNumberSet(validSet);
|
|
123
|
+
}).not.toThrow();
|
|
124
|
+
|
|
125
|
+
// Invalid input - should throw
|
|
126
|
+
expect(() => {
|
|
127
|
+
assertIsNumberSet('not a set');
|
|
128
|
+
}).toThrow('Expected <Set>, got <string> type value');
|
|
129
|
+
expect(() => {
|
|
130
|
+
assertIsNumberSet([1, 2, 3]);
|
|
131
|
+
}).toThrow('Expected <Set>, got <object> type value');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('SetType with custom typeName', () => {
|
|
135
|
+
const CustomSet = SetType(string(''), {
|
|
136
|
+
typeName: 'CustomStringSet',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(CustomSet.typeName).toBe('CustomStringSet');
|
|
140
|
+
|
|
141
|
+
// Error message should include custom type name
|
|
142
|
+
const result = CustomSet.validate('not a set');
|
|
143
|
+
expect(Result.isErr(result)).toBe(true);
|
|
144
|
+
if (Result.isErr(result)) {
|
|
145
|
+
expect(result.value[0]?.typeName).toBe('CustomStringSet');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('SetType with complex element types', () => {
|
|
150
|
+
const UserType = record({
|
|
151
|
+
id: number(0),
|
|
152
|
+
name: string(''),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const UserSet = SetType(UserType);
|
|
156
|
+
type UserSet = TypeOf<typeof UserSet>;
|
|
157
|
+
|
|
158
|
+
// Type test
|
|
159
|
+
expectType<UserSet, ReadonlySet<Readonly<{ id: number; name: string }>>>('=');
|
|
160
|
+
|
|
161
|
+
const user1 = { id: 1, name: 'Alice' };
|
|
162
|
+
const user2 = { id: 2, name: 'Bob' };
|
|
163
|
+
const validSet = new Set([user1, user2]);
|
|
164
|
+
|
|
165
|
+
// is() test
|
|
166
|
+
expect(UserSet.is(validSet)).toBe(true);
|
|
167
|
+
|
|
168
|
+
// validate() test
|
|
169
|
+
const result = UserSet.validate(validSet);
|
|
170
|
+
expect(Result.isOk(result)).toBe(true);
|
|
171
|
+
|
|
172
|
+
// Invalid element
|
|
173
|
+
const invalidSet = new Set([
|
|
174
|
+
user1,
|
|
175
|
+
{ id: 'invalid', name: 'Charlie' }, // Invalid id type
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
expect(UserSet.is(invalidSet)).toBe(false);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('SetType defaultValue', () => {
|
|
182
|
+
const StringSet = SetType(string(''));
|
|
183
|
+
|
|
184
|
+
expect(StringSet.defaultValue).toStrictEqual(new Set());
|
|
185
|
+
expect(StringSet.defaultValue.size).toBe(0);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('SetType ensures uniqueness', () => {
|
|
189
|
+
const NumberSet = SetType(number(0));
|
|
190
|
+
|
|
191
|
+
// Set with duplicate values (Set automatically handles uniqueness)
|
|
192
|
+
const setWithDuplicates = new Set([1, 2, 2, 3, 3, 3]);
|
|
193
|
+
|
|
194
|
+
expect(setWithDuplicates.size).toBe(3); // Only unique values
|
|
195
|
+
expect(NumberSet.is(setWithDuplicates)).toBe(true);
|
|
196
|
+
|
|
197
|
+
const result = NumberSet.validate(setWithDuplicates);
|
|
198
|
+
expect(Result.isOk(result)).toBe(true);
|
|
199
|
+
});
|
package/src/predefined/index.mts
CHANGED