ts-fortress 2.0.4 → 2.1.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
@@ -541,6 +541,35 @@ const UserProfileType = t.record({
541
541
  }),
542
542
  ),
543
543
  });
544
+
545
+ // Strict validation (disallow excess properties)
546
+ const StrictUserType = t.record(
547
+ {
548
+ id: t.string(''),
549
+ name: t.string(''),
550
+ },
551
+ {
552
+ allowExcessProperties: false, // Reject any properties not defined in schema
553
+ },
554
+ );
555
+
556
+ // Permissive validation (allow excess properties) - this is the default
557
+ const PermissiveUserType = t.record(
558
+ {
559
+ id: t.string(''),
560
+ name: t.string(''),
561
+ },
562
+ {
563
+ allowExcessProperties: true, // Allow additional properties (default behavior)
564
+ },
565
+ );
566
+
567
+ // Example usage
568
+ const strictData = { id: '123', name: 'John', extra: 'not allowed' };
569
+ console.log(StrictUserType.is(strictData)); // false - 'extra' property causes rejection
570
+
571
+ const permissiveData = { id: '123', name: 'John', extra: 'allowed' };
572
+ console.log(PermissiveUserType.is(permissiveData)); // true - 'extra' property is allowed
544
573
  ```
545
574
 
546
575
  ### Branded Types
@@ -649,6 +678,31 @@ try {
649
678
  } catch (error) {
650
679
  console.error('Validation failed:', error.message);
651
680
  }
681
+
682
+ // Excess property validation example
683
+ const StrictType = t.record(
684
+ {
685
+ name: t.string(''),
686
+ age: t.number(0),
687
+ },
688
+ {
689
+ allowExcessProperties: false,
690
+ },
691
+ );
692
+
693
+ const dataWithExcess = { name: 'John', age: 30, extra: 'not allowed' };
694
+ const strictResult = StrictType.validate(dataWithExcess);
695
+
696
+ if (Result.isErr(strictResult)) {
697
+ console.log(strictResult.value);
698
+ // [{
699
+ // path: ['extra'],
700
+ // actualValue: 'not allowed',
701
+ // expectedType: '{ name: string, age: number }',
702
+ // message: 'Excess property "extra" is not allowed',
703
+ // typeName: '{ name: string, age: number }'
704
+ // }]
705
+ }
652
706
  ```
653
707
 
654
708
  ### ValidationError Structure
@@ -684,7 +738,8 @@ type ValidationError = Readonly<{
684
738
 
685
739
  ### Objects
686
740
 
687
- - `t.record(schema)` - Object validation
741
+ - `t.record(schema, options?)` - Object validation
742
+ - `options.allowExcessProperties?: boolean` - Allow properties not defined in schema (default: true)
688
743
  - `t.keyValueRecord(keyType, valueType)` - Corresponding to the `Record<K, V>` type
689
744
  - `t.partial(recordType)` - Make all fields optional
690
745
  - `t.optional(type)` - Optional field wrapper
@@ -1,6 +1,8 @@
1
1
  import { type Type, type TypeOf } from '../type.mjs';
2
2
  export declare const record: <const R extends Record<string, Type<unknown>>>(source: R, options?: Partial<Readonly<{
3
3
  typeName: string;
4
+ /** @default true */
5
+ allowExcessProperties: boolean;
4
6
  }>>) => Type<RecordTypeValue<R>>;
5
7
  type RecordTypeValue<R extends Record<string, Type<unknown>>> = TsFortressInternal.RecordTypeValueImpl<R>;
6
8
  declare namespace TsFortressInternal {
@@ -1 +1 @@
1
- {"version":3,"file":"record.d.mts","sourceRoot":"","sources":["../../src/record/record.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AASrD,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAClE,QAAQ,CAAC,EACT,UAAU,OAAO,CACf,QAAQ,CAAC;IACP,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CACH,KACA,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAgFzB,CAAC;AAEF,KAAK,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAC1D,kBAAkB,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAE5C,kBAAU,kBAAkB,CAAC;IAC3B,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IACrE,sBAAsB,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjD,KAAK,sBAAsB,CACzB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EACvC,YAAY,SAAS,MAAM,CAAC,IAC1B,QAAQ,CACV;SACG,GAAG,IAAI,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;KACvC,GAAG;SACD,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;KACxD,CACF,CAAC;IAEF,KAAK,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI;SAC9D,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;YAAE,QAAQ,EAAE,IAAI,CAAA;SAAE,GAAG,CAAC,GAAG,KAAK;KAC5D,CAAC,MAAM,CAAC,CAAC,CAAC;;CACZ"}
1
+ {"version":3,"file":"record.d.mts","sourceRoot":"","sources":["../../src/record/record.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AASrD,eAAO,MAAM,MAAM,GAAI,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAClE,QAAQ,CAAC,EACT,UAAU,OAAO,CACf,QAAQ,CAAC;IACP,QAAQ,EAAE,MAAM,CAAC;IAEjB,oBAAoB;IACpB,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC,CACH,KACA,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAwGzB,CAAC;AAEF,KAAK,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAC1D,kBAAkB,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAE5C,kBAAU,kBAAkB,CAAC;IAC3B,MAAM,MAAM,mBAAmB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IACrE,sBAAsB,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjD,KAAK,sBAAsB,CACzB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EACvC,YAAY,SAAS,MAAM,CAAC,IAC1B,QAAQ,CACV;SACG,GAAG,IAAI,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;KACvC,GAAG;SACD,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;KACxD,CACF,CAAC;IAEF,KAAK,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI;SAC9D,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS;YAAE,QAAQ,EAAE,IAAI,CAAA;SAAE,GAAG,CAAC,GAAG,KAAK;KAC5D,CAAC,MAAM,CAAC,CAAC,CAAC;;CACZ"}
@@ -5,10 +5,12 @@ import { createIsFn } from '../utils/create-is-fn.mjs';
5
5
  import { createPrimitiveValidationError, prependPathToValidationErrors } from '../validation-error.mjs';
6
6
 
7
7
  const record = (source, options) => {
8
+ const sourceKeys = new Set(Object.keys(source));
8
9
  const typeNameFilled = options?.typeName ??
9
10
  `{ ${Object.entries(source)
10
11
  .map(([k, v]) => `${k}: ${v.typeName}`)
11
12
  .join(', ')} }`;
13
+ const allowExcessProperties = options?.allowExcessProperties ?? true;
12
14
  const defaultValue =
13
15
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
14
16
  Object.fromEntries(Object.entries(source).map(([key, value]) => tp(key, value.defaultValue)));
@@ -22,7 +24,7 @@ const record = (source, options) => {
22
24
  }),
23
25
  ]);
24
26
  }
25
- const errors = Object.entries(source).flatMap(([k, valueType]) => {
27
+ const defaultErrors = Object.entries(source).flatMap(([k, valueType]) => {
26
28
  if (!Object.hasOwn(a, k)) {
27
29
  if (source[k]?.optional === true)
28
30
  return [];
@@ -43,8 +45,22 @@ const record = (source, options) => {
43
45
  }
44
46
  return [];
45
47
  });
46
- if (errors.length > 0) {
47
- return Result.err(errors);
48
+ // Check for excess properties if allowExcessProperties is false
49
+ if (!allowExcessProperties) {
50
+ const excessKeys = Object.keys(a).filter((key) => !sourceKeys.has(key));
51
+ if (excessKeys.length > 0) {
52
+ const excessErrors = excessKeys.map((key) => ({
53
+ path: [key],
54
+ actualValue: a[key],
55
+ typeName: typeNameFilled,
56
+ expectedType: typeNameFilled,
57
+ message: `Excess property "${key}" is not allowed`,
58
+ }));
59
+ return Result.err([...defaultErrors, ...excessErrors]);
60
+ }
61
+ }
62
+ if (defaultErrors.length > 0) {
63
+ return Result.err(defaultErrors);
48
64
  }
49
65
  // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
50
66
  return Result.ok(a);
@@ -1 +1 @@
1
- {"version":3,"file":"record.mjs","sources":["../../src/record/record.mts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"record.mjs","sources":["../../src/record/record.mts"],"sourcesContent":[null],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-fortress",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -1,7 +1,10 @@
1
1
  import { expectType, Result } from 'ts-data-forge';
2
2
  import { number } from '../primitives/index.mjs';
3
3
  import { type TypeOf } from '../type.mjs';
4
- import { validationErrorsToMessages } from '../validation-error.mjs';
4
+ import {
5
+ type ValidationError,
6
+ validationErrorsToMessages,
7
+ } from '../validation-error.mjs';
5
8
  import { arrayOfLength } from './array-of-length.mjs';
6
9
 
7
10
  describe('arrayOfLength', () => {
@@ -74,6 +77,18 @@ describe('arrayOfLength', () => {
74
77
  });
75
78
 
76
79
  describe('validate', () => {
80
+ test('truthy case', () => {
81
+ const ys: unknown = [5, 6, 7, 8];
82
+
83
+ const result = xs.validate(ys);
84
+ expectType<typeof result, Result<Xs, readonly ValidationError[]>>('=');
85
+ expect(Result.isOk(result)).toBe(true);
86
+
87
+ if (Result.isOk(result)) {
88
+ expect(result.value).toStrictEqual([5, 6, 7, 8]);
89
+ }
90
+ });
91
+
77
92
  test('falsy case 1', () => {
78
93
  const ys: unknown = [];
79
94
 
@@ -1,7 +1,10 @@
1
1
  import { expectType, Result } from 'ts-data-forge';
2
2
  import { number } from '../primitives/index.mjs';
3
3
  import { type TypeOf } from '../type.mjs';
4
- import { validationErrorsToMessages } from '../validation-error.mjs';
4
+ import {
5
+ type ValidationError,
6
+ validationErrorsToMessages,
7
+ } from '../validation-error.mjs';
5
8
  import { array } from './array.mjs';
6
9
 
7
10
  describe('array', () => {
@@ -58,6 +61,18 @@ describe('array', () => {
58
61
  });
59
62
 
60
63
  describe('validate', () => {
64
+ test('truthy case', () => {
65
+ const ys: unknown = [1, 2, 3];
66
+
67
+ const result = xs.validate(ys);
68
+ expectType<typeof result, Result<Xs, readonly ValidationError[]>>('=');
69
+ expect(Result.isOk(result)).toBe(true);
70
+
71
+ if (Result.isOk(result)) {
72
+ expect(result.value).toStrictEqual([1, 2, 3]);
73
+ }
74
+ });
75
+
61
76
  test('falsy case', () => {
62
77
  const ys: unknown = ['1', '', 3];
63
78
 
@@ -1,7 +1,10 @@
1
1
  import { expectType, Result } from 'ts-data-forge';
2
2
  import { number } from '../primitives/index.mjs';
3
3
  import { type TypeOf } from '../type.mjs';
4
- import { validationErrorsToMessages } from '../validation-error.mjs';
4
+ import {
5
+ type ValidationError,
6
+ validationErrorsToMessages,
7
+ } from '../validation-error.mjs';
5
8
  import { nonEmptyArray } from './non-empty-array.mjs';
6
9
 
7
10
  describe('nonEmptyArray', () => {
@@ -74,6 +77,18 @@ describe('nonEmptyArray', () => {
74
77
  });
75
78
 
76
79
  describe('validate', () => {
80
+ test('truthy case', () => {
81
+ const ys: unknown = [1, 2, 3];
82
+
83
+ const result = xs.validate(ys);
84
+ expectType<typeof result, Result<Xs, readonly ValidationError[]>>('=');
85
+ expect(Result.isOk(result)).toBe(true);
86
+
87
+ if (Result.isOk(result)) {
88
+ expect(result.value).toStrictEqual([1, 2, 3]);
89
+ }
90
+ });
91
+
77
92
  test('falsy case 1', () => {
78
93
  const ys: unknown = [];
79
94
 
@@ -2,7 +2,10 @@ import { expectType, Result } from 'ts-data-forge';
2
2
  import { number, numberLiteral, stringLiteral } from '../primitives/index.mjs';
3
3
  import { record } from '../record/index.mjs';
4
4
  import { type TypeOf } from '../type.mjs';
5
- import { validationErrorsToMessages } from '../validation-error.mjs';
5
+ import {
6
+ type ValidationError,
7
+ validationErrorsToMessages,
8
+ } from '../validation-error.mjs';
6
9
  import { tuple } from './tuple.mjs';
7
10
 
8
11
  describe('tuple', () => {
@@ -72,6 +75,20 @@ describe('tuple', () => {
72
75
  });
73
76
 
74
77
  describe('validate', () => {
78
+ test('truthy case', () => {
79
+ const x: unknown = [{ x: 1, y: 2 }, 3, '2'];
80
+
81
+ const result = targetType.validate(x);
82
+ expectType<typeof result, Result<TargetType, readonly ValidationError[]>>(
83
+ '=',
84
+ );
85
+ expect(Result.isOk(result)).toBe(true);
86
+
87
+ if (Result.isOk(result)) {
88
+ expect(result.value).toStrictEqual([{ x: 1, y: 2 }, 3, '2']);
89
+ }
90
+ });
91
+
75
92
  test('falsy case - not array', () => {
76
93
  const x: unknown = 'not an array';
77
94
 
@@ -3,7 +3,10 @@ import { uintRange } from '../enum/index.mjs';
3
3
  import { number } from '../primitives/index.mjs';
4
4
  import { record } from '../record/index.mjs';
5
5
  import { type TypeOf } from '../type.mjs';
6
- import { validationErrorsToMessages } from '../validation-error.mjs';
6
+ import {
7
+ type ValidationError,
8
+ validationErrorsToMessages,
9
+ } from '../validation-error.mjs';
7
10
  import { intersection } from './intersection.mjs';
8
11
 
9
12
  describe('intersection', () => {
@@ -74,6 +77,19 @@ describe('intersection', () => {
74
77
  });
75
78
 
76
79
  describe('validate', () => {
80
+ test('truthy case', () => {
81
+ const result = targetType.validate({ x: 0, y: 1, z: 2, w: 3 });
82
+ expectType<
83
+ typeof result,
84
+ Result<TargetType, readonly ValidationError[]>
85
+ >('=');
86
+ expect(Result.isOk(result)).toBe(true);
87
+
88
+ if (Result.isOk(result)) {
89
+ expect(result.value).toStrictEqual({ x: 0, y: 1, z: 2, w: 3 });
90
+ }
91
+ });
92
+
77
93
  test('falsy case', () => {
78
94
  const result = targetType.validate({ x: 0, y: 1 });
79
95
  expect(Result.isErr(result)).toBe(true);
@@ -165,6 +181,19 @@ describe('intersection', () => {
165
181
  });
166
182
 
167
183
  describe('validate', () => {
184
+ test('truthy case', () => {
185
+ const result = targetType.validate(0);
186
+ expectType<
187
+ typeof result,
188
+ Result<TargetType, readonly ValidationError[]>
189
+ >('=');
190
+ expect(Result.isOk(result)).toBe(true);
191
+
192
+ if (Result.isOk(result)) {
193
+ expect(result.value).toBe(0);
194
+ }
195
+ });
196
+
168
197
  test('falsy case', () => {
169
198
  const result = targetType.validate('aaa');
170
199
  expect(Result.isErr(result)).toBe(true);
@@ -278,6 +307,19 @@ describe('intersection', () => {
278
307
  });
279
308
 
280
309
  describe('validate', () => {
310
+ test('truthy case', () => {
311
+ const result = targetType.validate(3);
312
+ expectType<
313
+ typeof result,
314
+ Result<TargetType, readonly ValidationError[]>
315
+ >('=');
316
+ expect(Result.isOk(result)).toBe(true);
317
+
318
+ if (Result.isOk(result)) {
319
+ expect(result.value).toBe(3);
320
+ }
321
+ });
322
+
281
323
  test('falsy case', () => {
282
324
  const result = targetType.validate(7);
283
325
  expect(Result.isErr(result)).toBe(true);
@@ -2,7 +2,10 @@ import { expectType, Result } from 'ts-data-forge';
2
2
  import { number } from '../primitives/index.mjs';
3
3
  import { record } from '../record/index.mjs';
4
4
  import { type TypeOf } from '../type.mjs';
5
- import { validationErrorsToMessages } from '../validation-error.mjs';
5
+ import {
6
+ type ValidationError,
7
+ validationErrorsToMessages,
8
+ } from '../validation-error.mjs';
6
9
  import { mergeRecords } from './merge-records.mjs';
7
10
 
8
11
  describe('mergeRecords', () => {
@@ -67,6 +70,18 @@ describe('mergeRecords', () => {
67
70
  });
68
71
 
69
72
  describe('validate', () => {
73
+ test('truthy case', () => {
74
+ const result = targetType.validate({ x: 0, y: 1, z: 2, w: 3 });
75
+ expectType<typeof result, Result<TargetType, readonly ValidationError[]>>(
76
+ '=',
77
+ );
78
+ expect(Result.isOk(result)).toBe(true);
79
+
80
+ if (Result.isOk(result)) {
81
+ expect(result.value).toStrictEqual({ x: 0, y: 1, z: 2, w: 3 });
82
+ }
83
+ });
84
+
70
85
  test('falsy case', () => {
71
86
  const result = targetType.validate({ x: 0, y: 1 });
72
87
  expect(Result.isErr(result)).toBe(true);
@@ -2,7 +2,10 @@ import { expectType, Result } from 'ts-data-forge';
2
2
  import { number, numberLiteral, stringLiteral } from '../primitives/index.mjs';
3
3
  import { record } from '../record/index.mjs';
4
4
  import { type TypeOf } from '../type.mjs';
5
- import { validationErrorsToMessages } from '../validation-error.mjs';
5
+ import {
6
+ type ValidationError,
7
+ validationErrorsToMessages,
8
+ } from '../validation-error.mjs';
6
9
  import { union } from './union.mjs';
7
10
 
8
11
  describe('union', () => {
@@ -70,6 +73,18 @@ describe('union', () => {
70
73
  });
71
74
 
72
75
  describe('validate', () => {
76
+ test('truthy case', () => {
77
+ const result = targetType.validate(3);
78
+ expectType<typeof result, Result<TargetType, readonly ValidationError[]>>(
79
+ '=',
80
+ );
81
+ expect(Result.isOk(result)).toBe(true);
82
+
83
+ if (Result.isOk(result)) {
84
+ expect(result.value).toBe(3);
85
+ }
86
+ });
87
+
73
88
  test('falsy case', () => {
74
89
  const result = targetType.validate(5);
75
90
  expect(Result.isErr(result)).toBe(true);
@@ -1,6 +1,9 @@
1
1
  import { expectType, Result } from 'ts-data-forge';
2
2
  import { type TypeOf } from '../type.mjs';
3
- import { validationErrorsToMessages } from '../validation-error.mjs';
3
+ import {
4
+ type ValidationError,
5
+ validationErrorsToMessages,
6
+ } from '../validation-error.mjs';
4
7
  import { enumType } from './enum.mjs';
5
8
 
6
9
  describe('enumType', () => {
@@ -41,6 +44,18 @@ describe('enumType', () => {
41
44
  });
42
45
 
43
46
  describe('validate', () => {
47
+ test('truthy case', () => {
48
+ const result = targetType.validate(3);
49
+ expectType<typeof result, Result<TargetType, readonly ValidationError[]>>(
50
+ '=',
51
+ );
52
+ expect(Result.isOk(result)).toBe(true);
53
+
54
+ if (Result.isOk(result)) {
55
+ expect(result.value).toBe(3);
56
+ }
57
+ });
58
+
44
59
  test('falsy case', () => {
45
60
  const result = targetType.validate(5);
46
61
  expect(Result.isErr(result)).toBe(true);
@@ -1,6 +1,9 @@
1
1
  import { expectType, Result } from 'ts-data-forge';
2
2
  import { type TypeOf } from '../type.mjs';
3
- import { validationErrorsToMessages } from '../validation-error.mjs';
3
+ import {
4
+ type ValidationError,
5
+ validationErrorsToMessages,
6
+ } from '../validation-error.mjs';
4
7
  import { uintRange } from './uint-range.mjs';
5
8
 
6
9
  describe('uintRange', () => {
@@ -44,6 +47,16 @@ describe('uintRange', () => {
44
47
  });
45
48
 
46
49
  describe('validate', () => {
50
+ test('truthy case', () => {
51
+ const result = month.validate(5);
52
+ expectType<typeof result, Result<Month, readonly ValidationError[]>>('=');
53
+ expect(Result.isOk(result)).toBe(true);
54
+
55
+ if (Result.isOk(result)) {
56
+ expect(result.value).toBe(5);
57
+ }
58
+ });
59
+
47
60
  test('falsy case', () => {
48
61
  const result = month.validate(13);
49
62
  expect(Result.isErr(result)).toBe(true);
@@ -0,0 +1,170 @@
1
+ import { expectType, Result } from 'ts-data-forge';
2
+ import { type TypeOf } from '../type.mjs';
3
+ import { nullType } from './null.mjs';
4
+
5
+ describe('nullType', () => {
6
+ type Null = TypeOf<typeof nullType>;
7
+
8
+ expectType<Null, null>('=');
9
+ expectType<typeof nullType.defaultValue, Null>('=');
10
+
11
+ describe('default value', () => {
12
+ test('is null', () => {
13
+ expect(nullType.defaultValue).toBeNull();
14
+ });
15
+ });
16
+
17
+ describe('is', () => {
18
+ test('null returns true', () => {
19
+ const value: unknown = null;
20
+
21
+ if (nullType.is(value)) {
22
+ expectType<typeof value, Null>('=');
23
+ } else {
24
+ expectType<typeof value, {} | undefined>('=');
25
+ }
26
+
27
+ expect(nullType.is(value)).toBe(true);
28
+ });
29
+
30
+ test('undefined returns false', () => {
31
+ const value: unknown = undefined;
32
+
33
+ if (nullType.is(value)) {
34
+ expectType<typeof value, Null>('=');
35
+ } else {
36
+ expectType<typeof value, {} | undefined>('=');
37
+ }
38
+
39
+ expect(nullType.is(value)).toBe(false);
40
+ });
41
+
42
+ test('other values return false', () => {
43
+ const values = [0, '', false, Number.NaN, {}, [], 'null', 42, true];
44
+
45
+ for (const value of values) {
46
+ const v: unknown = value;
47
+
48
+ if (nullType.is(v)) {
49
+ expectType<typeof v, Null>('=');
50
+ } else {
51
+ expectType<typeof v, {} | undefined>('=');
52
+ }
53
+
54
+ expect(nullType.is(v)).toBe(false);
55
+ }
56
+ });
57
+ });
58
+
59
+ describe('assertIs', () => {
60
+ test('null does not throw', () => {
61
+ const value: unknown = null;
62
+ expect(() => {
63
+ nullType.assertIs(value);
64
+ }).not.toThrow();
65
+ });
66
+
67
+ test('non-null throws', () => {
68
+ const value: unknown = undefined;
69
+ expect(() => {
70
+ nullType.assertIs(value);
71
+ }).toThrow('Expected null, got undefined');
72
+ });
73
+ });
74
+
75
+ describe('cast', () => {
76
+ test('null returns null', () => {
77
+ const value: unknown = null;
78
+ const result = nullType.cast(value);
79
+ expect(result).toBeNull();
80
+ });
81
+
82
+ test('non-null throws error', () => {
83
+ const value: unknown = undefined;
84
+ expect(() => nullType.cast(value)).toThrow(
85
+ 'Expected null, got undefined',
86
+ );
87
+ });
88
+
89
+ test('any non-null value throws error', () => {
90
+ const values = [0, '', false, undefined, 42, 'hello'];
91
+
92
+ for (const value of values) {
93
+ expect(() => nullType.cast(value)).toThrow(
94
+ /Expected null, got (number|string|boolean|undefined)/u,
95
+ );
96
+ }
97
+ });
98
+ });
99
+
100
+ describe('fill', () => {
101
+ test('null returns null', () => {
102
+ const value: unknown = null;
103
+ const result = nullType.fill(value);
104
+ expect(result).toBeNull();
105
+ });
106
+
107
+ test('undefined returns default (null)', () => {
108
+ const value: unknown = undefined;
109
+ const result = nullType.fill(value);
110
+ expect(result).toBeNull();
111
+ });
112
+
113
+ test('other values return default (null)', () => {
114
+ const values = [0, '', false, 42, 'hello', {}];
115
+
116
+ for (const value of values) {
117
+ const result = nullType.fill(value);
118
+ expect(result).toBeNull();
119
+ }
120
+ });
121
+ });
122
+
123
+ describe('validate', () => {
124
+ test('null is valid', () => {
125
+ const value: unknown = null;
126
+ const result = nullType.validate(value);
127
+
128
+ expect(Result.isOk(result)).toBe(true);
129
+ if (Result.isOk(result)) {
130
+ expect(result.value).toBeNull();
131
+ }
132
+ });
133
+
134
+ test('undefined is invalid', () => {
135
+ const value: unknown = undefined;
136
+ const result = nullType.validate(value);
137
+
138
+ expect(Result.isErr(result)).toBe(true);
139
+ if (Result.isErr(result)) {
140
+ expect(result.value).toStrictEqual([
141
+ {
142
+ path: [],
143
+ actualValue: undefined,
144
+ expectedType: 'null',
145
+ typeName: 'null',
146
+ message: undefined,
147
+ },
148
+ ]);
149
+ }
150
+ });
151
+
152
+ test('other values are invalid', () => {
153
+ const value: unknown = 0;
154
+ const result = nullType.validate(value);
155
+
156
+ expect(Result.isErr(result)).toBe(true);
157
+ if (Result.isErr(result)) {
158
+ expect(result.value).toStrictEqual([
159
+ {
160
+ path: [],
161
+ actualValue: 0,
162
+ expectedType: 'null',
163
+ typeName: 'null',
164
+ message: undefined,
165
+ },
166
+ ]);
167
+ }
168
+ });
169
+ });
170
+ });