ts-fortress 2.1.0 → 2.2.1

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,51 @@
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> = TypeEq<OptionalKeys, never> extends true ? Readonly<{
11
+ [key in Exclude<keyof A, OptionalKeys>]: TypeOf<A[key]>;
12
+ }> : TypeEq<keyof A, OptionalKeys> extends true ? Readonly<MergeIntersection<{
13
+ [key in OptionalKeys]?: TypeOf<A[key]>;
14
+ }>> : Readonly<MergeIntersection<{
11
15
  [key in OptionalKeys]?: TypeOf<A[key]>;
12
16
  } & {
13
17
  [key in Exclude<keyof A, OptionalKeys>]: TypeOf<A[key]>;
14
- }>;
15
- type OptionalTypeKeys<A extends Record<string, Type<unknown>>> = {
18
+ }>>;
19
+ type OptionalTypeKeys<A extends ReadonlyRecord<string, Type<unknown>>> = {
16
20
  [K in keyof A]: A[K] extends {
17
21
  optional: true;
18
22
  } ? K : never;
19
23
  }[keyof A];
20
24
  export {};
21
25
  }
26
+ /**
27
+ * Creates a strict record type that does not allow excess properties.
28
+ * This is an alias for `record(source, { allowExcessProperties: false })`.
29
+ *
30
+ * @param source - The record schema definition
31
+ * @param options - Optional configuration (allowExcessProperties will be overridden to false)
32
+ * @returns A Type that validates records without allowing excess properties
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * import { strictRecord, string, number } from 'ts-fortress';
37
+ *
38
+ * const User = strictRecord({
39
+ * name: string(),
40
+ * age: number()
41
+ * });
42
+ *
43
+ * User.is({ name: "John", age: 30 }); // true
44
+ * User.is({ name: "John", age: 30, extra: "not allowed" }); // false
45
+ * ```
46
+ */
47
+ export declare const strictRecord: <const R extends ReadonlyRecord<string, Type<unknown>>>(source: R, options?: Partial<Readonly<{
48
+ typeName: string;
49
+ }>>) => Type<RecordTypeValue<R>>;
22
50
  export {};
23
51
  //# 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,IAE5B,MAAM,CAAC,YAAY,EAAE,KAAK,CAAC,SAAS,IAAI,GACpC,QAAQ,CAAC;SACN,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;KACxD,CAAC,GACF,MAAM,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC,SAAS,IAAI,GACxC,QAAQ,CACN,iBAAiB,CAAC;SACf,GAAG,IAAI,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;KACvC,CAAC,CACH,GACD,QAAQ,CACN,iBAAiB,CACf;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,CACF,CAAC;IAEV,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.1",
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,25 +124,72 @@ 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
- > = Readonly<
138
- {
139
- [key in OptionalKeys]?: TypeOf<A[key]>;
140
- } & {
141
- [key in Exclude<keyof A, OptionalKeys>]: TypeOf<A[key]>;
142
- }
143
- >;
144
-
145
- type OptionalTypeKeys<A extends Record<string, Type<unknown>>> = {
138
+ > =
139
+ TypeEq<OptionalKeys, never> extends true
140
+ ? Readonly<{
141
+ [key in Exclude<keyof A, OptionalKeys>]: TypeOf<A[key]>;
142
+ }>
143
+ : TypeEq<keyof A, OptionalKeys> extends true
144
+ ? Readonly<
145
+ MergeIntersection<{
146
+ [key in OptionalKeys]?: TypeOf<A[key]>;
147
+ }>
148
+ >
149
+ : Readonly<
150
+ MergeIntersection<
151
+ {
152
+ [key in OptionalKeys]?: TypeOf<A[key]>;
153
+ } & {
154
+ [key in Exclude<keyof A, OptionalKeys>]: TypeOf<A[key]>;
155
+ }
156
+ >
157
+ >;
158
+
159
+ type OptionalTypeKeys<A extends ReadonlyRecord<string, Type<unknown>>> = {
146
160
  [K in keyof A]: A[K] extends { optional: true } ? K : never;
147
161
  }[keyof A];
148
162
  }
163
+
164
+ /**
165
+ * Creates a strict record type that does not allow excess properties.
166
+ * This is an alias for `record(source, { allowExcessProperties: false })`.
167
+ *
168
+ * @param source - The record schema definition
169
+ * @param options - Optional configuration (allowExcessProperties will be overridden to false)
170
+ * @returns A Type that validates records without allowing excess properties
171
+ *
172
+ * @example
173
+ * ```typescript
174
+ * import { strictRecord, string, number } from 'ts-fortress';
175
+ *
176
+ * const User = strictRecord({
177
+ * name: string(),
178
+ * age: number()
179
+ * });
180
+ *
181
+ * User.is({ name: "John", age: 30 }); // true
182
+ * User.is({ name: "John", age: 30, extra: "not allowed" }); // false
183
+ * ```
184
+ */
185
+ export const strictRecord = <
186
+ const R extends ReadonlyRecord<string, Type<unknown>>,
187
+ >(
188
+ source: R,
189
+ options?: Partial<
190
+ Readonly<{
191
+ typeName: string;
192
+ }>
193
+ >,
194
+ ): Type<RecordTypeValue<R>> =>
195
+ 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
+ });