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 +25 -1
- package/dist/index.mjs +1 -1
- package/dist/record/index.mjs +1 -1
- package/dist/record/key-value-record.d.mts +1 -1
- package/dist/record/key-value-record.d.mts.map +1 -1
- package/dist/record/record.d.mts +34 -6
- package/dist/record/record.d.mts.map +1 -1
- package/dist/record/record.mjs +23 -1
- package/dist/record/record.mjs.map +1 -1
- package/package.json +1 -1
- package/src/record/key-value-record.mts +4 -4
- package/src/record/key-value-record.test.mts +1 -1
- package/src/record/record.mts +61 -14
- package/src/record/strict-record.test.mts +192 -0
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';
|
package/dist/record/index.mjs
CHANGED
|
@@ -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>> =
|
|
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,
|
|
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"}
|
package/dist/record/record.d.mts
CHANGED
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
import { type Type, type TypeOf } from '../type.mjs';
|
|
2
|
-
export declare const record: <const R extends
|
|
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
|
|
7
|
+
type RecordTypeValue<R extends ReadonlyRecord<string, Type<unknown>>> = TsFortressInternal.RecordTypeValueImpl<R>;
|
|
8
8
|
declare namespace TsFortressInternal {
|
|
9
|
-
export type RecordTypeValueImpl<R extends
|
|
10
|
-
type RecordTypeValueImplSub<A extends
|
|
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
|
|
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,
|
|
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"}
|
package/dist/record/record.mjs
CHANGED
|
@@ -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
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
type ValidationErrorWithMessage,
|
|
9
9
|
} from '../validation-error.mjs';
|
|
10
10
|
|
|
11
|
-
type RecordResultType<
|
|
12
|
-
|
|
13
|
-
|
|
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,
|
|
15
|
+
expectType<StrNumRecord, ReadonlyRecord<string, number>>('=');
|
|
16
16
|
|
|
17
17
|
expectType<typeof strNumRecord.defaultValue, StrNumRecord>('=');
|
|
18
18
|
|
package/src/record/record.mts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type ValidationErrorWithMessage,
|
|
9
9
|
} from '../validation-error.mjs';
|
|
10
10
|
|
|
11
|
-
export const record = <const R extends
|
|
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
|
|
127
|
+
type RecordTypeValue<R extends ReadonlyRecord<string, Type<unknown>>> =
|
|
128
128
|
TsFortressInternal.RecordTypeValueImpl<R>;
|
|
129
129
|
|
|
130
130
|
namespace TsFortressInternal {
|
|
131
|
-
export type RecordTypeValueImpl<
|
|
132
|
-
|
|
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
|
|
136
|
+
A extends ReadonlyRecord<string, Type<unknown>>,
|
|
136
137
|
OptionalKeys extends keyof A,
|
|
137
|
-
> =
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
+
});
|