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 CHANGED
@@ -7,11 +7,12 @@ TypeScript-first schema validation library with static type inference.
7
7
  [![License](https://img.shields.io/npm/l/ts-fortress.svg)](./LICENSE)
8
8
  [![codecov](https://codecov.io/gh/noshiro-pf/ts-fortress/graph/badge.svg?token=Y522SWQTPB)](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://github.com/colinhacks/zod), designed to provide type-safe schema validation with excellent TypeScript integration and static type inference.
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.numberLiteral(1), // or t.number(1) with default
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.stringLiteral('active');
637
- const versionType = t.number(1);
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
- // Union types
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
- // Complex unions
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.stringLiteral(value)` - Literal string types
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
- ### Branded Types
916
+ ### Refinement
913
917
 
914
- - `t.simpleBrandedString(brandName, defaultValue)` - Simple string branding
915
- - `t.simpleBrandedNumber(brandName, defaultValue)` - Simple number branding
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? })` - Integer range validation
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
 
@@ -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';
@@ -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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,4 +1,6 @@
1
1
  export * from './literal.mjs';
2
+ export * from './map.mjs';
2
3
  export * from './refine.mjs';
4
+ export * from './set.mjs';
3
5
  export * from './unknown.mjs';
4
6
  //# sourceMappingURL=index.d.mts.map
@@ -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,4 +1,6 @@
1
1
  export { literal } from './literal.mjs';
2
+ export { MapType } from './map.mjs';
2
3
  export { refine } from './refine.mjs';
4
+ export { SetType } from './set.mjs';
3
5
  export { unknown } from './unknown.mjs';
4
6
  //# sourceMappingURL=index.mjs.map
@@ -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,4 +1,5 @@
1
1
  export * from './int8.mjs';
2
2
  export * from './json.mjs';
3
+ export * from './nullable.mjs';
3
4
  export * from './uint8.mjs';
4
5
  //# sourceMappingURL=index.d.mts.map
@@ -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,4 +1,5 @@
1
1
  export { int8 } from './int8.mjs';
2
2
  export { JsonObject, JsonPrimitive, JsonValue } from './json.mjs';
3
+ export { nullable } from './nullable.mjs';
3
4
  export { uint8 } from './uint8.mjs';
4
5
  //# sourceMappingURL=index.mjs.map
@@ -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,3 @@
1
+ import { type Type } from '../type.mjs';
2
+ export declare const nullable: <T>(type: Type<T>) => Type<T | undefined>;
3
+ //# sourceMappingURL=nullable.d.mts.map
@@ -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,8 @@
1
+ import 'ts-data-forge';
2
+ import { union } from '../compose/union.mjs';
3
+ import { undefinedType } from '../primitives/undefined.mjs';
4
+
5
+ const nullable = (type) => union([type, undefinedType]);
6
+
7
+ export { nullable };
8
+ //# sourceMappingURL=nullable.mjs.map
@@ -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.1",
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.4",
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.0",
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.0",
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.49.0",
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.11",
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",
@@ -1,3 +1,5 @@
1
1
  export * from './literal.mjs';
2
+ export * from './map.mjs';
2
3
  export * from './refine.mjs';
4
+ export * from './set.mjs';
3
5
  export * from './unknown.mjs';
@@ -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
+ });
@@ -1,3 +1,4 @@
1
1
  export * from './int8.mjs';
2
2
  export * from './json.mjs';
3
+ export * from './nullable.mjs';
3
4
  export * from './uint8.mjs';
@@ -0,0 +1,6 @@
1
+ import { union } from '../compose/index.mjs';
2
+ import { undefinedType } from '../primitives/index.mjs';
3
+ import { type Type } from '../type.mjs';
4
+
5
+ export const nullable = <T,>(type: Type<T>): Type<T | undefined> =>
6
+ union([type, undefinedType]);