ts-fortress 2.1.0 → 2.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
@@ -553,6 +553,12 @@ const StrictUserType = t.record(
553
553
  },
554
554
  );
555
555
 
556
+ // Alternatively, use the strictRecord alias for cleaner syntax
557
+ const StrictUserTypeAlias = t.strictRecord({
558
+ id: t.string(''),
559
+ name: t.string(''),
560
+ });
561
+
556
562
  // Permissive validation (allow excess properties) - this is the default
557
563
  const PermissiveUserType = t.record(
558
564
  {
@@ -564,12 +570,29 @@ const PermissiveUserType = t.record(
564
570
  },
565
571
  );
566
572
 
567
- // Example usage
573
+ // Example usage - both StrictUserType and StrictUserTypeAlias behave identically
568
574
  const strictData = { id: '123', name: 'John', extra: 'not allowed' };
569
575
  console.log(StrictUserType.is(strictData)); // false - 'extra' property causes rejection
576
+ console.log(StrictUserTypeAlias.is(strictData)); // false - same as above
570
577
 
571
578
  const permissiveData = { id: '123', name: 'John', extra: 'allowed' };
572
579
  console.log(PermissiveUserType.is(permissiveData)); // true - 'extra' property is allowed
580
+
581
+ // strictRecord provides cleaner syntax for strict validation
582
+ const UserSchema = t.strictRecord({
583
+ name: t.string(''),
584
+ email: t.string(''),
585
+ age: t.number(0),
586
+ });
587
+
588
+ // Validation examples
589
+ UserSchema.is({ name: 'John', email: 'john@example.com', age: 30 }); // ✅ true
590
+ UserSchema.is({
591
+ name: 'John',
592
+ email: 'john@example.com',
593
+ age: 30,
594
+ role: 'admin',
595
+ }); // ❌ false - excess property
573
596
  ```
574
597
 
575
598
  ### Branded Types
@@ -740,6 +763,7 @@ type ValidationError = Readonly<{
740
763
 
741
764
  - `t.record(schema, options?)` - Object validation
742
765
  - `options.allowExcessProperties?: boolean` - Allow properties not defined in schema (default: true)
766
+ - `t.strictRecord(schema, options?)` - Object validation with strict mode (alias for `record` with `allowExcessProperties: false`)
743
767
  - `t.keyValueRecord(keyType, valueType)` - Corresponding to the `Record<K, V>` type
744
768
  - `t.partial(recordType)` - Make all fields optional
745
769
  - `t.optional(type)` - Optional field wrapper
package/dist/index.mjs CHANGED
@@ -39,7 +39,7 @@ export { omit } from './record/omit.mjs';
39
39
  export { isOptionalProperty, optional } from './record/optional.mjs';
40
40
  export { partial } from './record/partial.mjs';
41
41
  export { pick } from './record/pick.mjs';
42
- export { record } from './record/record.mjs';
42
+ export { record, strictRecord } from './record/record.mjs';
43
43
  export { unknown } from './unknown.mjs';
44
44
  export { createAssertFn } from './utils/create-assert-fn.mjs';
45
45
  export { createCastFn } from './utils/create-cast-fn.mjs';
@@ -4,5 +4,5 @@ export { omit } from './omit.mjs';
4
4
  export { isOptionalProperty, optional } from './optional.mjs';
5
5
  export { partial } from './partial.mjs';
6
6
  export { pick } from './pick.mjs';
7
- export { record } from './record.mjs';
7
+ export { record, strictRecord } from './record.mjs';
8
8
  //# sourceMappingURL=index.mjs.map
@@ -1,5 +1,5 @@
1
1
  import { type Type, type TypeOf } from '../type.mjs';
2
- type RecordResultType<K extends Type<string>, V extends Type<unknown>> = Record<TypeOf<K>, TypeOf<V>>;
2
+ type RecordResultType<K extends Type<string>, V extends Type<unknown>> = ReadonlyRecord<TypeOf<K>, TypeOf<V>>;
3
3
  export declare const keyValueRecord: <K extends Type<string>, V extends Type<unknown>>(keyType: K, valueType: V, options?: Readonly<{
4
4
  typeName?: string;
5
5
  }>) => Type<RecordResultType<K, V>>;
@@ -1 +1 @@
1
- {"version":3,"file":"key-value-record.d.mts","sourceRoot":"","sources":["../../src/record/key-value-record.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AASrD,KAAK,gBAAgB,CAAC,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAC7E,MAAM,CAAC,CAAC,CAAC,EACT,MAAM,CAAC,CAAC,CAAC,CACV,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EAC5E,SAAS,CAAC,EACV,WAAW,CAAC,EACZ,UAAU,QAAQ,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,KACxC,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAoF7B,CAAC"}
1
+ {"version":3,"file":"key-value-record.d.mts","sourceRoot":"","sources":["../../src/record/key-value-record.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,IAAI,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AASrD,KAAK,gBAAgB,CACnB,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,EACtB,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IACrB,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAEzC,eAAO,MAAM,cAAc,GAAI,CAAC,SAAS,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EAC5E,SAAS,CAAC,EACV,WAAW,CAAC,EACZ,UAAU,QAAQ,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,KACxC,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAoF7B,CAAC"}
@@ -1,23 +1,47 @@
1
1
  import { type Type, type TypeOf } from '../type.mjs';
2
- export declare const record: <const R extends Record<string, Type<unknown>>>(source: R, options?: Partial<Readonly<{
2
+ export declare const record: <const R extends ReadonlyRecord<string, Type<unknown>>>(source: R, options?: Partial<Readonly<{
3
3
  typeName: string;
4
4
  /** @default true */
5
5
  allowExcessProperties: boolean;
6
6
  }>>) => Type<RecordTypeValue<R>>;
7
- type RecordTypeValue<R extends Record<string, Type<unknown>>> = TsFortressInternal.RecordTypeValueImpl<R>;
7
+ type RecordTypeValue<R extends ReadonlyRecord<string, Type<unknown>>> = TsFortressInternal.RecordTypeValueImpl<R>;
8
8
  declare namespace TsFortressInternal {
9
- export type RecordTypeValueImpl<R extends Record<string, Type<unknown>>> = RecordTypeValueImplSub<R, OptionalTypeKeys<R>>;
10
- type RecordTypeValueImplSub<A extends Record<string, Type<unknown>>, OptionalKeys extends keyof A> = Readonly<{
9
+ export type RecordTypeValueImpl<R extends ReadonlyRecord<string, Type<unknown>>> = RecordTypeValueImplSub<R, OptionalTypeKeys<R>>;
10
+ type RecordTypeValueImplSub<A extends ReadonlyRecord<string, Type<unknown>>, OptionalKeys extends keyof A> = Readonly<{
11
11
  [key in OptionalKeys]?: TypeOf<A[key]>;
12
12
  } & {
13
13
  [key in Exclude<keyof A, OptionalKeys>]: TypeOf<A[key]>;
14
14
  }>;
15
- type OptionalTypeKeys<A extends Record<string, Type<unknown>>> = {
15
+ type OptionalTypeKeys<A extends ReadonlyRecord<string, Type<unknown>>> = {
16
16
  [K in keyof A]: A[K] extends {
17
17
  optional: true;
18
18
  } ? K : never;
19
19
  }[keyof A];
20
20
  export {};
21
21
  }
22
+ /**
23
+ * Creates a strict record type that does not allow excess properties.
24
+ * This is an alias for `record(source, { allowExcessProperties: false })`.
25
+ *
26
+ * @param source - The record schema definition
27
+ * @param options - Optional configuration (allowExcessProperties will be overridden to false)
28
+ * @returns A Type that validates records without allowing excess properties
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { strictRecord, string, number } from 'ts-fortress';
33
+ *
34
+ * const User = strictRecord({
35
+ * name: string(),
36
+ * age: number()
37
+ * });
38
+ *
39
+ * User.is({ name: "John", age: 30 }); // true
40
+ * User.is({ name: "John", age: 30, extra: "not allowed" }); // false
41
+ * ```
42
+ */
43
+ export declare const strictRecord: <const R extends ReadonlyRecord<string, Type<unknown>>>(source: R, options?: Partial<Readonly<{
44
+ typeName: string;
45
+ }>>) => Type<RecordTypeValue<R>>;
22
46
  export {};
23
47
  //# sourceMappingURL=record.d.mts.map
@@ -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;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"}
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,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAC1E,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,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAClE,kBAAkB,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC;AAE5C,kBAAU,kBAAkB,CAAC;IAC3B,MAAM,MAAM,mBAAmB,CAC7B,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAC7C,sBAAsB,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAC;IAEnD,KAAK,sBAAsB,CACzB,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAC/C,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,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI;SACtE,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;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,YAAY,GACvB,KAAK,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAErD,QAAQ,CAAC,EACT,UAAU,OAAO,CACf,QAAQ,CAAC;IACP,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CACH,KACA,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CACoC,CAAC"}
@@ -79,6 +79,28 @@ const record = (source, options) => {
79
79
  cast: createCastFn(validate),
80
80
  };
81
81
  };
82
+ /**
83
+ * Creates a strict record type that does not allow excess properties.
84
+ * This is an alias for `record(source, { allowExcessProperties: false })`.
85
+ *
86
+ * @param source - The record schema definition
87
+ * @param options - Optional configuration (allowExcessProperties will be overridden to false)
88
+ * @returns A Type that validates records without allowing excess properties
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * import { strictRecord, string, number } from 'ts-fortress';
93
+ *
94
+ * const User = strictRecord({
95
+ * name: string(),
96
+ * age: number()
97
+ * });
98
+ *
99
+ * User.is({ name: "John", age: 30 }); // true
100
+ * User.is({ name: "John", age: 30, extra: "not allowed" }); // false
101
+ * ```
102
+ */
103
+ const strictRecord = (source, options) => record(source, { ...options, allowExcessProperties: false });
82
104
 
83
- export { record };
105
+ export { record, strictRecord };
84
106
  //# sourceMappingURL=record.mjs.map
@@ -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.1.0",
3
+ "version": "2.2.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -8,10 +8,10 @@ import {
8
8
  type ValidationErrorWithMessage,
9
9
  } from '../validation-error.mjs';
10
10
 
11
- type RecordResultType<K extends Type<string>, V extends Type<unknown>> = Record<
12
- TypeOf<K>,
13
- TypeOf<V>
14
- >;
11
+ type RecordResultType<
12
+ K extends Type<string>,
13
+ V extends Type<unknown>,
14
+ > = ReadonlyRecord<TypeOf<K>, TypeOf<V>>;
15
15
 
16
16
  export const keyValueRecord = <K extends Type<string>, V extends Type<unknown>>(
17
17
  keyType: K,
@@ -12,7 +12,7 @@ describe('keyValueRecord', () => {
12
12
 
13
13
  type StrNumRecord = TypeOf<typeof strNumRecord>;
14
14
 
15
- expectType<StrNumRecord, Record<string, number>>('=');
15
+ expectType<StrNumRecord, ReadonlyRecord<string, number>>('=');
16
16
 
17
17
  expectType<typeof strNumRecord.defaultValue, StrNumRecord>('=');
18
18
 
@@ -8,7 +8,7 @@ import {
8
8
  type ValidationErrorWithMessage,
9
9
  } from '../validation-error.mjs';
10
10
 
11
- export const record = <const R extends Record<string, Type<unknown>>>(
11
+ export const record = <const R extends ReadonlyRecord<string, Type<unknown>>>(
12
12
  source: R,
13
13
  options?: Partial<
14
14
  Readonly<{
@@ -124,15 +124,16 @@ export const record = <const R extends Record<string, Type<unknown>>>(
124
124
  };
125
125
  };
126
126
 
127
- type RecordTypeValue<R extends Record<string, Type<unknown>>> =
127
+ type RecordTypeValue<R extends ReadonlyRecord<string, Type<unknown>>> =
128
128
  TsFortressInternal.RecordTypeValueImpl<R>;
129
129
 
130
130
  namespace TsFortressInternal {
131
- export type RecordTypeValueImpl<R extends Record<string, Type<unknown>>> =
132
- RecordTypeValueImplSub<R, OptionalTypeKeys<R>>;
131
+ export type RecordTypeValueImpl<
132
+ R extends ReadonlyRecord<string, Type<unknown>>,
133
+ > = RecordTypeValueImplSub<R, OptionalTypeKeys<R>>;
133
134
 
134
135
  type RecordTypeValueImplSub<
135
- A extends Record<string, Type<unknown>>,
136
+ A extends ReadonlyRecord<string, Type<unknown>>,
136
137
  OptionalKeys extends keyof A,
137
138
  > = Readonly<
138
139
  {
@@ -142,7 +143,40 @@ namespace TsFortressInternal {
142
143
  }
143
144
  >;
144
145
 
145
- type OptionalTypeKeys<A extends Record<string, Type<unknown>>> = {
146
+ type OptionalTypeKeys<A extends ReadonlyRecord<string, Type<unknown>>> = {
146
147
  [K in keyof A]: A[K] extends { optional: true } ? K : never;
147
148
  }[keyof A];
148
149
  }
150
+
151
+ /**
152
+ * Creates a strict record type that does not allow excess properties.
153
+ * This is an alias for `record(source, { allowExcessProperties: false })`.
154
+ *
155
+ * @param source - The record schema definition
156
+ * @param options - Optional configuration (allowExcessProperties will be overridden to false)
157
+ * @returns A Type that validates records without allowing excess properties
158
+ *
159
+ * @example
160
+ * ```typescript
161
+ * import { strictRecord, string, number } from 'ts-fortress';
162
+ *
163
+ * const User = strictRecord({
164
+ * name: string(),
165
+ * age: number()
166
+ * });
167
+ *
168
+ * User.is({ name: "John", age: 30 }); // true
169
+ * User.is({ name: "John", age: 30, extra: "not allowed" }); // false
170
+ * ```
171
+ */
172
+ export const strictRecord = <
173
+ const R extends ReadonlyRecord<string, Type<unknown>>,
174
+ >(
175
+ source: R,
176
+ options?: Partial<
177
+ Readonly<{
178
+ typeName: string;
179
+ }>
180
+ >,
181
+ ): Type<RecordTypeValue<R>> =>
182
+ record(source, { ...options, allowExcessProperties: false });
@@ -0,0 +1,192 @@
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 { validationErrorsToMessages } from '../validation-error.mjs';
5
+ import { record, strictRecord } from './record.mjs';
6
+
7
+ describe('strictRecord', () => {
8
+ const userType = strictRecord({
9
+ name: string(''),
10
+ age: number(0),
11
+ });
12
+
13
+ type User = TypeOf<typeof userType>;
14
+ expectType<User, Readonly<{ name: string; age: number }>>('=');
15
+
16
+ describe('is', () => {
17
+ test('valid data without excess properties', () => {
18
+ const validUser = { name: 'John', age: 30 };
19
+
20
+ expect(userType.is(validUser)).toBe(true);
21
+ });
22
+
23
+ test('rejects data with excess properties', () => {
24
+ const userWithExtra = { name: 'John', age: 30, extra: 'not allowed' };
25
+
26
+ expect(userType.is(userWithExtra)).toBe(false);
27
+ });
28
+
29
+ test('rejects invalid property types', () => {
30
+ const invalidUser = { name: 'John', age: 'thirty' };
31
+
32
+ expect(userType.is(invalidUser)).toBe(false);
33
+ });
34
+ });
35
+
36
+ describe('validate', () => {
37
+ test('truthy case - valid data', () => {
38
+ const validUser = { name: 'John', age: 30 };
39
+
40
+ const result = userType.validate(validUser);
41
+ expect(Result.isOk(result)).toBe(true);
42
+
43
+ if (Result.isOk(result)) {
44
+ expectType<typeof result.value, User>('=');
45
+ expect(result.value).toStrictEqual({
46
+ name: 'John',
47
+ age: 30,
48
+ });
49
+ }
50
+ });
51
+
52
+ test('falsy case - excess properties', () => {
53
+ const userWithExtra = { name: 'John', age: 30, extra: 'not allowed' };
54
+
55
+ const result = userType.validate(userWithExtra);
56
+ expect(Result.isErr(result)).toBe(true);
57
+
58
+ if (Result.isErr(result)) {
59
+ expect(result.value).toStrictEqual([
60
+ {
61
+ path: ['extra'],
62
+ actualValue: 'not allowed',
63
+ expectedType: '{ name: string, age: number }',
64
+ typeName: '{ name: string, age: number }',
65
+ message: 'Excess property "extra" is not allowed',
66
+ },
67
+ ]);
68
+ expect(validationErrorsToMessages(result.value)).toStrictEqual([
69
+ 'Excess property "extra" is not allowed at extra',
70
+ ]);
71
+ }
72
+ });
73
+
74
+ test('falsy case - invalid property type', () => {
75
+ const invalidUser = { name: 'John', age: 'thirty' };
76
+
77
+ const result = userType.validate(invalidUser);
78
+ expect(Result.isErr(result)).toBe(true);
79
+
80
+ if (Result.isErr(result)) {
81
+ expect(result.value).toStrictEqual([
82
+ {
83
+ path: ['age'],
84
+ actualValue: 'thirty',
85
+ expectedType: 'number',
86
+ typeName: 'number',
87
+ message: undefined,
88
+ },
89
+ ]);
90
+ }
91
+ });
92
+
93
+ test('falsy case - missing required property', () => {
94
+ const incompleteUser = { name: 'John' };
95
+
96
+ const result = userType.validate(incompleteUser);
97
+ expect(Result.isErr(result)).toBe(true);
98
+
99
+ if (Result.isErr(result)) {
100
+ expect(result.value).toStrictEqual([
101
+ {
102
+ path: ['age'],
103
+ actualValue: incompleteUser,
104
+ expectedType: '{ name: string, age: number }',
105
+ typeName: '{ name: string, age: number }',
106
+ message: 'Missing required key "age"',
107
+ },
108
+ ]);
109
+ }
110
+ });
111
+ });
112
+
113
+ describe('comparison with regular record', () => {
114
+ const regularRecord = record({
115
+ name: string(''),
116
+ age: number(0),
117
+ });
118
+
119
+ test('regular record allows excess properties', () => {
120
+ const dataWithExtra = { name: 'John', age: 30, extra: 'allowed' };
121
+
122
+ expect(regularRecord.is(dataWithExtra)).toBe(true);
123
+ expect(
124
+ strictRecord({ name: string(''), age: number(0) }).is(dataWithExtra),
125
+ ).toBe(false);
126
+ });
127
+
128
+ test('both reject invalid property types', () => {
129
+ const invalidData = { name: 'John', age: 'thirty' };
130
+
131
+ expect(regularRecord.is(invalidData)).toBe(false);
132
+ expect(
133
+ strictRecord({ name: string(''), age: number(0) }).is(invalidData),
134
+ ).toBe(false);
135
+ });
136
+ });
137
+
138
+ describe('with custom typeName', () => {
139
+ test('uses custom typeName in errors', () => {
140
+ const customUserType = strictRecord(
141
+ {
142
+ name: string(''),
143
+ age: number(0),
144
+ },
145
+ { typeName: 'User' },
146
+ );
147
+
148
+ const result = customUserType.validate({
149
+ name: 'John',
150
+ age: 30,
151
+ extra: 'not allowed',
152
+ });
153
+ expect(Result.isErr(result)).toBe(true);
154
+
155
+ if (Result.isErr(result)) {
156
+ expect(result.value[0]?.expectedType).toBe('User');
157
+ expect(result.value[0]?.typeName).toBe('User');
158
+ }
159
+ });
160
+ });
161
+
162
+ describe('default values', () => {
163
+ test('has correct default value', () => {
164
+ expect(userType.defaultValue).toStrictEqual({
165
+ name: '',
166
+ age: 0,
167
+ });
168
+ });
169
+ });
170
+
171
+ describe('fill', () => {
172
+ test('fills missing properties with defaults', () => {
173
+ const partialData = { name: 'John' };
174
+
175
+ const result = userType.fill(partialData);
176
+ expect(result).toStrictEqual({
177
+ name: 'John',
178
+ age: 0,
179
+ });
180
+ });
181
+
182
+ test('preserves all valid properties', () => {
183
+ const validData = { name: 'John', age: 30 };
184
+
185
+ const result = userType.fill(validData);
186
+ expect(result).toStrictEqual({
187
+ name: 'John',
188
+ age: 30,
189
+ });
190
+ });
191
+ });
192
+ });