ts-fortress 4.2.1 → 4.3.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/dist/entry-point.mjs +1 -0
- package/dist/entry-point.mjs.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/record/index.d.mts +1 -0
- package/dist/record/index.d.mts.map +1 -1
- package/dist/record/index.mjs +1 -0
- package/dist/record/index.mjs.map +1 -1
- package/dist/record/optional.d.mts +1 -0
- package/dist/record/optional.d.mts.map +1 -1
- package/dist/record/optional.mjs.map +1 -1
- package/dist/record/required.d.mts +22 -0
- package/dist/record/required.d.mts.map +1 -0
- package/dist/record/required.mjs +33 -0
- package/dist/record/required.mjs.map +1 -0
- package/package.json +1 -1
- package/src/record/index.mts +1 -0
- package/src/record/optional.mts +5 -0
- package/src/record/partial.mts +1 -1
- package/src/record/required.mts +246 -0
- package/src/record/required.test.mts +600 -0
package/dist/entry-point.mjs
CHANGED
|
@@ -52,6 +52,7 @@ export { isOptionalProperty, optional } from './record/optional.mjs';
|
|
|
52
52
|
export { partial } from './record/partial.mjs';
|
|
53
53
|
export { pick } from './record/pick.mjs';
|
|
54
54
|
export { record, strictRecord } from './record/record.mjs';
|
|
55
|
+
export { required } from './record/required.mjs';
|
|
55
56
|
export { valueof } from './record/valueof.mjs';
|
|
56
57
|
export { createAssertFn } from './utils/create-assert-fn.mjs';
|
|
57
58
|
export { createCastFn } from './utils/create-cast-fn.mjs';
|
package/dist/entry-point.mjs.map
CHANGED
|
@@ -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
|
@@ -51,6 +51,7 @@ export { isOptionalProperty, optional } from './record/optional.mjs';
|
|
|
51
51
|
export { partial } from './record/partial.mjs';
|
|
52
52
|
export { pick } from './record/pick.mjs';
|
|
53
53
|
export { record, strictRecord } from './record/record.mjs';
|
|
54
|
+
export { required } from './record/required.mjs';
|
|
54
55
|
export { valueof } from './record/valueof.mjs';
|
|
55
56
|
import 'ts-data-forge';
|
|
56
57
|
export { createAssertFn } from './utils/create-assert-fn.mjs';
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/record/index.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/record/index.mts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/record/index.mts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC;AAC3B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC"}
|
package/dist/record/index.mjs
CHANGED
|
@@ -5,5 +5,6 @@ export { isOptionalProperty, optional } from './optional.mjs';
|
|
|
5
5
|
export { partial } from './partial.mjs';
|
|
6
6
|
export { pick } from './pick.mjs';
|
|
7
7
|
export { record, strictRecord } from './record.mjs';
|
|
8
|
+
export { required } from './required.mjs';
|
|
8
9
|
export { valueof } from './valueof.mjs';
|
|
9
10
|
//# 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":";;;;;;;;"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Type } from '../type.mjs';
|
|
2
2
|
export type OptionalPropertyType<T extends Type<unknown>> = T & PartiallyRequired<T, 'optional'>;
|
|
3
|
+
export type RequiredPropertyType<T extends Type<unknown>> = PartiallyOptional<T, 'optional'>;
|
|
3
4
|
export declare const optional: <T extends Type<unknown>>(t: T) => OptionalPropertyType<T>;
|
|
4
5
|
export declare const isOptionalProperty: <T extends Type<unknown>>(t: T) => t is OptionalPropertyType<T>;
|
|
5
6
|
//# sourceMappingURL=optional.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optional.d.mts","sourceRoot":"","sources":["../../src/record/optional.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAC3D,iBAAiB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;AAEnC,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EAC9C,GAAG,CAAC,KACH,oBAAoB,CAAC,CAAC,CAGvB,CAAC;AAEH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EACxD,GAAG,CAAC,KACH,CAAC,IAAI,oBAAoB,CAAC,CAAC,CACuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"optional.d.mts","sourceRoot":"","sources":["../../src/record/optional.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,GAC3D,iBAAiB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;AAEnC,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,IAAI,iBAAiB,CAC3E,CAAC,EACD,UAAU,CACX,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EAC9C,GAAG,CAAC,KACH,oBAAoB,CAAC,CAAC,CAGvB,CAAC;AAEH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,EACxD,GAAG,CAAC,KACH,CAAC,IAAI,oBAAoB,CAAC,CAAC,CACuB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"optional.mjs","sources":["../../src/record/optional.mts"],"sourcesContent":[null],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"optional.mjs","sources":["../../src/record/optional.mts"],"sourcesContent":[null],"names":[],"mappings":"MAUa,QAAQ,GAAG,CACtB,CAAI,MACyB;AAC7B,IAAA,GAAG,CAAC;AACJ,IAAA,QAAQ,EAAE,IAAI;AACf,CAAA;MAEY,kBAAkB,GAAG,CAChC,CAAI,KAEJ,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK;;;;"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type RecordType, type Type } from '../type.mjs';
|
|
2
|
+
import { type RequiredPropertyType } from './optional.mjs';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a Required type. If keysToBeRequired is set, only those keys are
|
|
5
|
+
* made required, otherwise, all properties are made required.
|
|
6
|
+
*/
|
|
7
|
+
export declare const required: <const R extends ReadonlyRecord<string, Type<unknown>>, const KeysToBeRequired extends NonEmptyArray<keyof R & string>>(recordType: RecordType<R>, options?: PartialReadonly<{
|
|
8
|
+
keysToBeRequired: KeysToBeRequired;
|
|
9
|
+
typeName: string;
|
|
10
|
+
/** @default true */
|
|
11
|
+
allowExcessProperties: boolean;
|
|
12
|
+
}>) => RequiredType<R, KeysToBeRequired>;
|
|
13
|
+
type RequiredTypeShape<R extends ReadonlyRecord<string, Type<unknown>>, KeysToBeRequired extends NonEmptyArray<keyof R & string> | undefined> = TypeEq<KeysToBeRequired, undefined> extends true ? FullyRequiredType<R> : PartiallyRequiredType<R, ArrayElement<KeysToBeRequired>>;
|
|
14
|
+
type FullyRequiredType<R extends ReadonlyRecord<string, Type<unknown>>> = {
|
|
15
|
+
readonly [P in keyof R]: RequiredPropertyType<R[P]>;
|
|
16
|
+
};
|
|
17
|
+
type PartiallyRequiredType<R extends ReadonlyRecord<string, Type<unknown>>, K extends keyof R> = {
|
|
18
|
+
readonly [P in keyof R]: P extends K ? RequiredPropertyType<R[P]> : R[P];
|
|
19
|
+
};
|
|
20
|
+
export type RequiredType<R extends ReadonlyRecord<string, Type<unknown>>, KeysToBeRequired extends NonEmptyArray<keyof R & string> | undefined> = RecordType<RequiredTypeShape<R, KeysToBeRequired>>;
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=required.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"required.d.mts","sourceRoot":"","sources":["../../src/record/required.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAEzD,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AAGxB;;;GAGG;AACH,eAAO,MAAM,QAAQ,GACnB,KAAK,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EACrD,KAAK,CAAC,gBAAgB,SAAS,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,EAE9D,YAAY,UAAU,CAAC,CAAC,CAAC,EACzB,UAAU,eAAe,CAAC;IACxB,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,QAAQ,EAAE,MAAM,CAAC;IAEjB,oBAAoB;IACpB,qBAAqB,EAAE,OAAO,CAAC;CAChC,CAAC,KACD,YAAY,CAAC,CAAC,EAAE,gBAAgB,CA2BlC,CAAC;AAaF,KAAK,iBAAiB,CACpB,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAC/C,gBAAgB,SAAS,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,SAAS,IAEpE,MAAM,CAAC,gBAAgB,EAAE,SAAS,CAAC,SAAS,IAAI,GAC5C,iBAAiB,CAAC,CAAC,CAAC,GACpB,qBAAqB,CAAC,CAAC,EAAE,YAAY,CAAC,gBAAgB,CAAC,CAAC,CAAC;AAE/D,KAAK,iBAAiB,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI;IACxE,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACpD,CAAC;AAEF,KAAK,qBAAqB,CACxB,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAC/C,CAAC,SAAS,MAAM,CAAC,IACf;IACF,QAAQ,EAAE,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACzE,CAAC;AAEF,MAAM,MAAM,YAAY,CACtB,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAC/C,gBAAgB,SAAS,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,SAAS,IAClE,UAAU,CAAC,iBAAiB,CAAC,CAAC,EAAE,gBAAgB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Obj } from 'ts-data-forge';
|
|
2
|
+
import { toUnionKeyString } from '../utils/to-union-string.mjs';
|
|
3
|
+
import { isOptionalProperty } from './optional.mjs';
|
|
4
|
+
import { record } from './record.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a Required type. If keysToBeRequired is set, only those keys are
|
|
8
|
+
* made required, otherwise, all properties are made required.
|
|
9
|
+
*/
|
|
10
|
+
const required = (recordType, options) => {
|
|
11
|
+
const typeNameFilled = options?.typeName ??
|
|
12
|
+
(options?.keysToBeRequired === undefined
|
|
13
|
+
? `Required<${recordType.typeName}>`
|
|
14
|
+
: `PartiallyRequired<${recordType.typeName}, ${toUnionKeyString(options.keysToBeRequired)}>`);
|
|
15
|
+
const keysToBeRequired = new Set(options?.keysToBeRequired ?? Object.keys(recordType.shape));
|
|
16
|
+
const requiredShape =
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
18
|
+
Object.fromEntries(Object.entries(recordType.shape).map(([k, v]) => [k, keysToBeRequired.has(k) ? makeRequired(v) : v]));
|
|
19
|
+
return record(requiredShape, {
|
|
20
|
+
typeName: typeNameFilled,
|
|
21
|
+
allowExcessProperties: options?.allowExcessProperties ?? recordType.allowExcessProperties,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Makes an optional property required by removing the optional flag.
|
|
26
|
+
*/
|
|
27
|
+
const makeRequired = (t) => isOptionalProperty(t)
|
|
28
|
+
? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
29
|
+
Obj.omit(t, ['optional'])
|
|
30
|
+
: t;
|
|
31
|
+
|
|
32
|
+
export { required };
|
|
33
|
+
//# sourceMappingURL=required.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"required.mjs","sources":["../../src/record/required.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;;AAUA;;;AAGG;MACU,QAAQ,GAAG,CAItB,UAAyB,EACzB,OAME,KACmC;AACrC,IAAA,MAAM,cAAc,GAClB,OAAO,EAAE,QAAQ;AACjB,SAAC,OAAO,EAAE,gBAAgB,KAAK;AAC7B,cAAE,CAAA,SAAA,EAAY,UAAU,CAAC,QAAQ,CAAA,CAAA;AACjC,cAAE,CAAA,kBAAA,EAAqB,UAAU,CAAC,QAAQ,CAAA,EAAA,EAAK,gBAAgB,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAA,CAAA,CAAG,CAAC;AAEjG,IAAA,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CACnD,OAAO,EAAE,gBAAgB,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAC3D;AAED,IAAA,MAAM,aAAa;;IAEjB,MAAM,CAAC,WAAW,CAChB,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,GAAG,CAClC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,CAAC,CAAU,CACxE,CAIF;IAEH,OAAO,MAAM,CAAC,aAAa,EAAE;AAC3B,QAAA,QAAQ,EAAE,cAAc;AACxB,QAAA,qBAAqB,EACnB,OAAO,EAAE,qBAAqB,IAAI,UAAU,CAAC,qBAAqB;AACrE,KAAA,CAAC;AACJ;AAEA;;AAEG;AACH,MAAM,YAAY,GAAG,CACnB,CAAI,KAEJ,kBAAkB,CAAC,CAAC;AAClB;QACG,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC;MACzB,CAAC;;;;"}
|
package/package.json
CHANGED
package/src/record/index.mts
CHANGED
package/src/record/optional.mts
CHANGED
|
@@ -3,6 +3,11 @@ import { type Type } from '../type.mjs';
|
|
|
3
3
|
export type OptionalPropertyType<T extends Type<unknown>> = T &
|
|
4
4
|
PartiallyRequired<T, 'optional'>;
|
|
5
5
|
|
|
6
|
+
export type RequiredPropertyType<T extends Type<unknown>> = PartiallyOptional<
|
|
7
|
+
T,
|
|
8
|
+
'optional'
|
|
9
|
+
>;
|
|
10
|
+
|
|
6
11
|
export const optional = <T extends Type<unknown>>(
|
|
7
12
|
t: T,
|
|
8
13
|
): OptionalPropertyType<T> => ({
|
package/src/record/partial.mts
CHANGED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { expectType, Obj } from 'ts-data-forge';
|
|
2
|
+
import { type RecordType, type Type } from '../type.mjs';
|
|
3
|
+
import { toUnionKeyString } from '../utils/index.mjs';
|
|
4
|
+
import {
|
|
5
|
+
isOptionalProperty,
|
|
6
|
+
type OptionalPropertyType,
|
|
7
|
+
type RequiredPropertyType,
|
|
8
|
+
} from './optional.mjs';
|
|
9
|
+
import { record } from './record.mjs';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a Required type. If keysToBeRequired is set, only those keys are
|
|
13
|
+
* made required, otherwise, all properties are made required.
|
|
14
|
+
*/
|
|
15
|
+
export const required = <
|
|
16
|
+
const R extends ReadonlyRecord<string, Type<unknown>>,
|
|
17
|
+
const KeysToBeRequired extends NonEmptyArray<keyof R & string>,
|
|
18
|
+
>(
|
|
19
|
+
recordType: RecordType<R>,
|
|
20
|
+
options?: PartialReadonly<{
|
|
21
|
+
keysToBeRequired: KeysToBeRequired;
|
|
22
|
+
typeName: string;
|
|
23
|
+
|
|
24
|
+
/** @default true */
|
|
25
|
+
allowExcessProperties: boolean;
|
|
26
|
+
}>,
|
|
27
|
+
): RequiredType<R, KeysToBeRequired> => {
|
|
28
|
+
const typeNameFilled: string =
|
|
29
|
+
options?.typeName ??
|
|
30
|
+
(options?.keysToBeRequired === undefined
|
|
31
|
+
? `Required<${recordType.typeName}>`
|
|
32
|
+
: `PartiallyRequired<${recordType.typeName}, ${toUnionKeyString(options.keysToBeRequired)}>`);
|
|
33
|
+
|
|
34
|
+
const keysToBeRequired: ReadonlySet<string> = new Set(
|
|
35
|
+
options?.keysToBeRequired ?? Object.keys(recordType.shape),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const requiredShape =
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
40
|
+
Object.fromEntries(
|
|
41
|
+
Object.entries(recordType.shape).map(
|
|
42
|
+
([k, v]) => [k, keysToBeRequired.has(k) ? makeRequired(v) : v] as const,
|
|
43
|
+
),
|
|
44
|
+
) satisfies ReadonlyRecord<string, Type<unknown>> as RequiredTypeShape<
|
|
45
|
+
R,
|
|
46
|
+
KeysToBeRequired
|
|
47
|
+
>;
|
|
48
|
+
|
|
49
|
+
return record(requiredShape, {
|
|
50
|
+
typeName: typeNameFilled,
|
|
51
|
+
allowExcessProperties:
|
|
52
|
+
options?.allowExcessProperties ?? recordType.allowExcessProperties,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Makes an optional property required by removing the optional flag.
|
|
58
|
+
*/
|
|
59
|
+
const makeRequired = <T extends Type<unknown>>(
|
|
60
|
+
t: T,
|
|
61
|
+
): RequiredPropertyType<T> =>
|
|
62
|
+
isOptionalProperty(t)
|
|
63
|
+
? // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
64
|
+
(Obj.omit(t, ['optional']) as RequiredPropertyType<T>)
|
|
65
|
+
: t;
|
|
66
|
+
|
|
67
|
+
type RequiredTypeShape<
|
|
68
|
+
R extends ReadonlyRecord<string, Type<unknown>>,
|
|
69
|
+
KeysToBeRequired extends NonEmptyArray<keyof R & string> | undefined,
|
|
70
|
+
> =
|
|
71
|
+
TypeEq<KeysToBeRequired, undefined> extends true
|
|
72
|
+
? FullyRequiredType<R>
|
|
73
|
+
: PartiallyRequiredType<R, ArrayElement<KeysToBeRequired>>;
|
|
74
|
+
|
|
75
|
+
type FullyRequiredType<R extends ReadonlyRecord<string, Type<unknown>>> = {
|
|
76
|
+
readonly [P in keyof R]: RequiredPropertyType<R[P]>;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type PartiallyRequiredType<
|
|
80
|
+
R extends ReadonlyRecord<string, Type<unknown>>,
|
|
81
|
+
K extends keyof R,
|
|
82
|
+
> = {
|
|
83
|
+
readonly [P in keyof R]: P extends K ? RequiredPropertyType<R[P]> : R[P];
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type RequiredType<
|
|
87
|
+
R extends ReadonlyRecord<string, Type<unknown>>,
|
|
88
|
+
KeysToBeRequired extends NonEmptyArray<keyof R & string> | undefined,
|
|
89
|
+
> = RecordType<RequiredTypeShape<R, KeysToBeRequired>>;
|
|
90
|
+
|
|
91
|
+
expectType<
|
|
92
|
+
RequiredTypeShape<
|
|
93
|
+
{
|
|
94
|
+
a: OptionalPropertyType<Type<0>>;
|
|
95
|
+
b: Type<1>;
|
|
96
|
+
c: OptionalPropertyType<Type<2>>;
|
|
97
|
+
},
|
|
98
|
+
['a']
|
|
99
|
+
>,
|
|
100
|
+
Readonly<{
|
|
101
|
+
a: Type<0>;
|
|
102
|
+
b: Type<1>;
|
|
103
|
+
c: OptionalPropertyType<Type<2>>;
|
|
104
|
+
}>
|
|
105
|
+
>('=');
|
|
106
|
+
|
|
107
|
+
expectType<
|
|
108
|
+
RecordType<
|
|
109
|
+
Readonly<{
|
|
110
|
+
a: Type<0>;
|
|
111
|
+
b: Type<1>;
|
|
112
|
+
c: Type<2>;
|
|
113
|
+
}>
|
|
114
|
+
>,
|
|
115
|
+
Type<
|
|
116
|
+
Readonly<{
|
|
117
|
+
a: 0;
|
|
118
|
+
b: 1;
|
|
119
|
+
c: 2;
|
|
120
|
+
}>
|
|
121
|
+
> &
|
|
122
|
+
Readonly<{
|
|
123
|
+
shape: Readonly<{
|
|
124
|
+
a: Type<0>;
|
|
125
|
+
b: Type<1>;
|
|
126
|
+
c: Type<2>;
|
|
127
|
+
}>;
|
|
128
|
+
allowExcessProperties: boolean;
|
|
129
|
+
}>
|
|
130
|
+
>('=');
|
|
131
|
+
|
|
132
|
+
expectType<
|
|
133
|
+
RecordType<
|
|
134
|
+
Readonly<{
|
|
135
|
+
a: Type<0>;
|
|
136
|
+
b: Type<1>;
|
|
137
|
+
c: OptionalPropertyType<Type<2>>;
|
|
138
|
+
}>
|
|
139
|
+
>,
|
|
140
|
+
Type<
|
|
141
|
+
Readonly<{
|
|
142
|
+
a: 0;
|
|
143
|
+
b: 1;
|
|
144
|
+
c?: 2;
|
|
145
|
+
}>
|
|
146
|
+
> &
|
|
147
|
+
Readonly<{
|
|
148
|
+
shape: Readonly<{
|
|
149
|
+
a: Type<0>;
|
|
150
|
+
b: Type<1>;
|
|
151
|
+
c: OptionalPropertyType<Type<2>>;
|
|
152
|
+
}>;
|
|
153
|
+
allowExcessProperties: boolean;
|
|
154
|
+
}>
|
|
155
|
+
>('=');
|
|
156
|
+
|
|
157
|
+
expectType<
|
|
158
|
+
RequiredType<
|
|
159
|
+
{
|
|
160
|
+
a: OptionalPropertyType<Type<0>>;
|
|
161
|
+
b: Type<1>;
|
|
162
|
+
c: OptionalPropertyType<Type<2>>;
|
|
163
|
+
},
|
|
164
|
+
['a']
|
|
165
|
+
>,
|
|
166
|
+
Type<
|
|
167
|
+
Readonly<{
|
|
168
|
+
a: 0;
|
|
169
|
+
b: 1;
|
|
170
|
+
c?: 2;
|
|
171
|
+
}>
|
|
172
|
+
> &
|
|
173
|
+
Readonly<{
|
|
174
|
+
shape: Readonly<{
|
|
175
|
+
a: Type<0>;
|
|
176
|
+
b: Type<1>;
|
|
177
|
+
c: OptionalPropertyType<Type<2>>;
|
|
178
|
+
}>;
|
|
179
|
+
allowExcessProperties: boolean;
|
|
180
|
+
}>
|
|
181
|
+
>('=');
|
|
182
|
+
|
|
183
|
+
expectType<
|
|
184
|
+
RequiredType<
|
|
185
|
+
{
|
|
186
|
+
a: OptionalPropertyType<Type<0>>;
|
|
187
|
+
b: OptionalPropertyType<Type<1>>;
|
|
188
|
+
c: Type<2>;
|
|
189
|
+
},
|
|
190
|
+
['a', 'b']
|
|
191
|
+
>,
|
|
192
|
+
Type<
|
|
193
|
+
Readonly<{
|
|
194
|
+
a: 0;
|
|
195
|
+
b: 1;
|
|
196
|
+
c: 2;
|
|
197
|
+
}>
|
|
198
|
+
> &
|
|
199
|
+
Readonly<{
|
|
200
|
+
shape: Readonly<{
|
|
201
|
+
a: Type<0>;
|
|
202
|
+
b: Type<1>;
|
|
203
|
+
c: Type<2>;
|
|
204
|
+
}>;
|
|
205
|
+
allowExcessProperties: boolean;
|
|
206
|
+
}>
|
|
207
|
+
>('=');
|
|
208
|
+
|
|
209
|
+
expectType<
|
|
210
|
+
RequiredType<
|
|
211
|
+
{
|
|
212
|
+
a: OptionalPropertyType<Type<0>>;
|
|
213
|
+
b: OptionalPropertyType<Type<1>>;
|
|
214
|
+
c: OptionalPropertyType<Type<2>>;
|
|
215
|
+
},
|
|
216
|
+
undefined
|
|
217
|
+
>,
|
|
218
|
+
Type<
|
|
219
|
+
Readonly<{
|
|
220
|
+
a: 0;
|
|
221
|
+
b: 1;
|
|
222
|
+
c: 2;
|
|
223
|
+
}>
|
|
224
|
+
> &
|
|
225
|
+
Readonly<{
|
|
226
|
+
shape: Readonly<{
|
|
227
|
+
a: Type<0>;
|
|
228
|
+
b: Type<1>;
|
|
229
|
+
c: Type<2>;
|
|
230
|
+
}>;
|
|
231
|
+
allowExcessProperties: boolean;
|
|
232
|
+
}>
|
|
233
|
+
>('=');
|
|
234
|
+
|
|
235
|
+
expectType<
|
|
236
|
+
RequiredType<
|
|
237
|
+
{
|
|
238
|
+
a: OptionalPropertyType<Type<0>>;
|
|
239
|
+
b: Type<1>;
|
|
240
|
+
c: OptionalPropertyType<Type<2>>;
|
|
241
|
+
},
|
|
242
|
+
// @ts-expect-error key "d" doesn't exist
|
|
243
|
+
['a', 'd']
|
|
244
|
+
>,
|
|
245
|
+
0
|
|
246
|
+
>('!=');
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { expectType, Result } from 'ts-data-forge';
|
|
2
|
+
import { number } from '../primitives/index.mjs';
|
|
3
|
+
import { type Type, type TypeOf } from '../type.mjs';
|
|
4
|
+
import {
|
|
5
|
+
type ValidationError,
|
|
6
|
+
validationErrorsToMessages,
|
|
7
|
+
} from '../utils/index.mjs';
|
|
8
|
+
import { optional, type OptionalPropertyType } from './optional.mjs';
|
|
9
|
+
import { record } from './record.mjs';
|
|
10
|
+
import { required, type RequiredType } from './required.mjs';
|
|
11
|
+
|
|
12
|
+
describe('required', () => {
|
|
13
|
+
describe('fully required', () => {
|
|
14
|
+
const ymdBase = record({
|
|
15
|
+
year: optional(number(1900)),
|
|
16
|
+
month: optional(number(1)),
|
|
17
|
+
date: optional(number(1)),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const ymd = required(ymdBase);
|
|
21
|
+
|
|
22
|
+
expectType<
|
|
23
|
+
typeof ymd,
|
|
24
|
+
RequiredType<
|
|
25
|
+
Readonly<{
|
|
26
|
+
year: OptionalPropertyType<Type<number>>;
|
|
27
|
+
month: OptionalPropertyType<Type<number>>;
|
|
28
|
+
date: OptionalPropertyType<Type<number>>;
|
|
29
|
+
}>,
|
|
30
|
+
undefined
|
|
31
|
+
>
|
|
32
|
+
>('=');
|
|
33
|
+
|
|
34
|
+
expectType<
|
|
35
|
+
typeof ymd,
|
|
36
|
+
Type<
|
|
37
|
+
Readonly<{
|
|
38
|
+
year: number;
|
|
39
|
+
month: number;
|
|
40
|
+
date: number;
|
|
41
|
+
}>
|
|
42
|
+
> &
|
|
43
|
+
Readonly<{
|
|
44
|
+
shape: Readonly<{
|
|
45
|
+
year: Type<number>;
|
|
46
|
+
month: Type<number>;
|
|
47
|
+
date: Type<number>;
|
|
48
|
+
}>;
|
|
49
|
+
allowExcessProperties: boolean;
|
|
50
|
+
}>
|
|
51
|
+
>('=');
|
|
52
|
+
|
|
53
|
+
type Ymd = TypeOf<typeof ymd>;
|
|
54
|
+
|
|
55
|
+
expectType<
|
|
56
|
+
Ymd,
|
|
57
|
+
Required<Readonly<{ year: number; month: number; date: number }>>
|
|
58
|
+
>('=');
|
|
59
|
+
|
|
60
|
+
expectType<typeof ymd.defaultValue, Ymd>('=');
|
|
61
|
+
|
|
62
|
+
describe('is', () => {
|
|
63
|
+
test('truthy case', () => {
|
|
64
|
+
const x: UnknownRecord = {
|
|
65
|
+
year: 2000,
|
|
66
|
+
month: 12,
|
|
67
|
+
date: 12,
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
if (ymd.is(x)) {
|
|
71
|
+
expectType<typeof x, Ymd>('=');
|
|
72
|
+
} else {
|
|
73
|
+
expectType<typeof x, UnknownRecord>('=');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
expect(ymd.is(x)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('falsy case - missing required property', () => {
|
|
80
|
+
const x: UnknownRecord = {
|
|
81
|
+
year: 2000,
|
|
82
|
+
month: 12,
|
|
83
|
+
// missing date - this should fail since date is now required
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (ymd.is(x)) {
|
|
87
|
+
expectType<typeof x, Ymd>('=');
|
|
88
|
+
} else {
|
|
89
|
+
expectType<typeof x, UnknownRecord>('=');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
expect(ymd.is(x)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('falsy case - wrong type', () => {
|
|
96
|
+
const x: UnknownRecord = {
|
|
97
|
+
year: 2000,
|
|
98
|
+
month: 'ab',
|
|
99
|
+
date: 'cd',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (ymd.is(x)) {
|
|
103
|
+
expectType<typeof x, Ymd>('=');
|
|
104
|
+
} else {
|
|
105
|
+
expectType<typeof x, UnknownRecord>('=');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
expect(ymd.is(x)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('validate', () => {
|
|
113
|
+
test('truthy case', () => {
|
|
114
|
+
const x: UnknownRecord = {
|
|
115
|
+
year: 2000,
|
|
116
|
+
month: 12,
|
|
117
|
+
date: 25,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const result = ymd.validate(x);
|
|
121
|
+
expectType<typeof result, Result<Ymd, readonly ValidationError[]>>('=');
|
|
122
|
+
expect(Result.isOk(result)).toBe(true);
|
|
123
|
+
|
|
124
|
+
if (Result.isOk(result)) {
|
|
125
|
+
expect(result.value).toStrictEqual({
|
|
126
|
+
year: 2000,
|
|
127
|
+
month: 12,
|
|
128
|
+
date: 25,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('validate returns input as-is for OK cases', () => {
|
|
134
|
+
const input: UnknownRecord = {
|
|
135
|
+
year: 2000,
|
|
136
|
+
month: 12,
|
|
137
|
+
date: 25,
|
|
138
|
+
};
|
|
139
|
+
const result = ymd.validate(input);
|
|
140
|
+
expect(Result.isOk(result)).toBe(true);
|
|
141
|
+
if (Result.isOk(result)) {
|
|
142
|
+
expect(result.value).toBe(input); // ✅ same reference
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test('falsy case - missing required property', () => {
|
|
147
|
+
const x: UnknownRecord = {
|
|
148
|
+
year: 2000,
|
|
149
|
+
month: 12,
|
|
150
|
+
// missing date - this should fail since date is now required
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const result = ymd.validate(x);
|
|
154
|
+
expect(Result.isErr(result)).toBe(true);
|
|
155
|
+
|
|
156
|
+
if (Result.isErr(result)) {
|
|
157
|
+
expect(result.value).toStrictEqual([
|
|
158
|
+
{
|
|
159
|
+
path: ['date'],
|
|
160
|
+
actualValue: { year: 2000, month: 12 },
|
|
161
|
+
expectedType:
|
|
162
|
+
'Required<{ year: number, month: number, date: number }>',
|
|
163
|
+
typeName:
|
|
164
|
+
'Required<{ year: number, month: number, date: number }>',
|
|
165
|
+
message: 'Missing required key "date"',
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
expect(validationErrorsToMessages(result.value)).toStrictEqual([
|
|
169
|
+
'Missing required key "date" at date',
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('truthy case with additional keys', () => {
|
|
175
|
+
const x: UnknownRecord = {
|
|
176
|
+
year: 2000,
|
|
177
|
+
month: 12,
|
|
178
|
+
date: 25,
|
|
179
|
+
aaa: 999,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const result = ymd.validate(x);
|
|
183
|
+
expectType<typeof result, Result<Ymd, readonly ValidationError[]>>('=');
|
|
184
|
+
expect(Result.isOk(result)).toBe(true);
|
|
185
|
+
|
|
186
|
+
if (Result.isOk(result)) {
|
|
187
|
+
expect(result.value).toStrictEqual({
|
|
188
|
+
year: 2000,
|
|
189
|
+
month: 12,
|
|
190
|
+
date: 25,
|
|
191
|
+
aaa: 999,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('validate returns input as-is for OK cases with additional keys', () => {
|
|
197
|
+
const input: UnknownRecord = {
|
|
198
|
+
year: 2000,
|
|
199
|
+
month: 12,
|
|
200
|
+
date: 25,
|
|
201
|
+
aaa: 999,
|
|
202
|
+
};
|
|
203
|
+
const result = ymd.validate(input);
|
|
204
|
+
expect(Result.isOk(result)).toBe(true);
|
|
205
|
+
if (Result.isOk(result)) {
|
|
206
|
+
expect(result.value).toBe(input); // ✅ same reference
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('falsy case - wrong type', () => {
|
|
211
|
+
const x: UnknownRecord = {
|
|
212
|
+
year: 2000,
|
|
213
|
+
month: 'ab',
|
|
214
|
+
date: 'cd',
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const result = ymd.validate(x);
|
|
218
|
+
expect(Result.isErr(result)).toBe(true);
|
|
219
|
+
|
|
220
|
+
if (Result.isErr(result)) {
|
|
221
|
+
expect(result.value).toStrictEqual([
|
|
222
|
+
{
|
|
223
|
+
path: ['month'],
|
|
224
|
+
actualValue: 'ab',
|
|
225
|
+
expectedType: 'number',
|
|
226
|
+
typeName: 'number',
|
|
227
|
+
message: undefined,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
path: ['date'],
|
|
231
|
+
actualValue: 'cd',
|
|
232
|
+
expectedType: 'number',
|
|
233
|
+
typeName: 'number',
|
|
234
|
+
message: undefined,
|
|
235
|
+
},
|
|
236
|
+
]);
|
|
237
|
+
expect(validationErrorsToMessages(result.value)).toStrictEqual([
|
|
238
|
+
'Expected <number> at month, got <string> type value "ab".',
|
|
239
|
+
'Expected <number> at date, got <string> type value "cd".',
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('falsy case - single wrong type', () => {
|
|
245
|
+
const x: UnknownRecord = {
|
|
246
|
+
year: 2000,
|
|
247
|
+
month: 'ab',
|
|
248
|
+
date: 12,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const result = ymd.validate(x);
|
|
252
|
+
expect(Result.isErr(result)).toBe(true);
|
|
253
|
+
|
|
254
|
+
if (Result.isErr(result)) {
|
|
255
|
+
expect(result.value).toStrictEqual([
|
|
256
|
+
{
|
|
257
|
+
path: ['month'],
|
|
258
|
+
actualValue: 'ab',
|
|
259
|
+
expectedType: 'number',
|
|
260
|
+
typeName: 'number',
|
|
261
|
+
message: undefined,
|
|
262
|
+
},
|
|
263
|
+
]);
|
|
264
|
+
expect(validationErrorsToMessages(result.value)).toStrictEqual([
|
|
265
|
+
'Expected <number> at month, got <string> type value "ab".',
|
|
266
|
+
]);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('fill', () => {
|
|
272
|
+
test('from an empty record', () => {
|
|
273
|
+
const x: UnknownRecord = {};
|
|
274
|
+
|
|
275
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
276
|
+
year: 1900,
|
|
277
|
+
month: 1,
|
|
278
|
+
date: 1,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('from a filled record', () => {
|
|
283
|
+
const x: UnknownRecord = {
|
|
284
|
+
year: 2000,
|
|
285
|
+
month: 999,
|
|
286
|
+
date: 999,
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
290
|
+
year: 2000,
|
|
291
|
+
month: 999,
|
|
292
|
+
date: 999,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('from a partial record', () => {
|
|
297
|
+
const x: UnknownRecord = {
|
|
298
|
+
year: 2000,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
302
|
+
year: 2000,
|
|
303
|
+
month: 1,
|
|
304
|
+
date: 1,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('from a partial record with excess property', () => {
|
|
309
|
+
const x: UnknownRecord = {
|
|
310
|
+
year: 2000,
|
|
311
|
+
aaaaa: 9999,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
315
|
+
year: 2000,
|
|
316
|
+
month: 1,
|
|
317
|
+
date: 1,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
describe('partially required', () => {
|
|
324
|
+
const ymdBase = record({
|
|
325
|
+
year: optional(number(1900)),
|
|
326
|
+
month: optional(number(1)),
|
|
327
|
+
date: optional(number(1)),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const ymd = required(ymdBase, { keysToBeRequired: ['year', 'month'] });
|
|
331
|
+
|
|
332
|
+
type Ymd = TypeOf<typeof ymd>;
|
|
333
|
+
|
|
334
|
+
expectType<Ymd, Readonly<{ year: number; month: number; date?: number }>>(
|
|
335
|
+
'=',
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
expectType<typeof ymd.defaultValue, Ymd>('=');
|
|
339
|
+
|
|
340
|
+
describe('is', () => {
|
|
341
|
+
test('truthy case - required fields only', () => {
|
|
342
|
+
const x: UnknownRecord = {
|
|
343
|
+
year: 2000,
|
|
344
|
+
month: 12,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
if (ymd.is(x)) {
|
|
348
|
+
expectType<typeof x, Ymd>('=');
|
|
349
|
+
} else {
|
|
350
|
+
expectType<typeof x, UnknownRecord>('=');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
expect(ymd.is(x)).toBe(true);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test('truthy case - with optional field', () => {
|
|
357
|
+
const x: UnknownRecord = {
|
|
358
|
+
year: 2000,
|
|
359
|
+
month: 12,
|
|
360
|
+
date: 15,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
if (ymd.is(x)) {
|
|
364
|
+
expectType<typeof x, Ymd>('=');
|
|
365
|
+
} else {
|
|
366
|
+
expectType<typeof x, UnknownRecord>('=');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
expect(ymd.is(x)).toBe(true);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test('falsy case - missing required field', () => {
|
|
373
|
+
const x: UnknownRecord = {
|
|
374
|
+
year: 2000,
|
|
375
|
+
// missing month
|
|
376
|
+
date: 15,
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
if (ymd.is(x)) {
|
|
380
|
+
expectType<typeof x, Ymd>('=');
|
|
381
|
+
} else {
|
|
382
|
+
expectType<typeof x, UnknownRecord>('=');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
expect(ymd.is(x)).toBe(false);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('falsy case - wrong type in required field', () => {
|
|
389
|
+
const x: UnknownRecord = {
|
|
390
|
+
year: 2000,
|
|
391
|
+
month: 'ab',
|
|
392
|
+
date: 'cd',
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
if (ymd.is(x)) {
|
|
396
|
+
expectType<typeof x, Ymd>('=');
|
|
397
|
+
} else {
|
|
398
|
+
expectType<typeof x, UnknownRecord>('=');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
expect(ymd.is(x)).toBe(false);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test('falsy case - wrong type in optional field', () => {
|
|
405
|
+
const x: UnknownRecord = {
|
|
406
|
+
year: 2000,
|
|
407
|
+
month: 12,
|
|
408
|
+
date: 'cd',
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
if (ymd.is(x)) {
|
|
412
|
+
expectType<typeof x, Ymd>('=');
|
|
413
|
+
} else {
|
|
414
|
+
expectType<typeof x, UnknownRecord>('=');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
expect(ymd.is(x)).toBe(false);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
describe('validate', () => {
|
|
422
|
+
test('truthy case - required fields only', () => {
|
|
423
|
+
const x: UnknownRecord = {
|
|
424
|
+
year: 2000,
|
|
425
|
+
month: 12,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const result = ymd.validate(x);
|
|
429
|
+
expectType<typeof result, Result<Ymd, readonly ValidationError[]>>('=');
|
|
430
|
+
expect(Result.isOk(result)).toBe(true);
|
|
431
|
+
|
|
432
|
+
if (Result.isOk(result)) {
|
|
433
|
+
expect(result.value).toStrictEqual({
|
|
434
|
+
year: 2000,
|
|
435
|
+
month: 12,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test('partiallyRequiredType validate returns input as-is for OK cases', () => {
|
|
441
|
+
const input: UnknownRecord = {
|
|
442
|
+
year: 2000,
|
|
443
|
+
month: 12,
|
|
444
|
+
};
|
|
445
|
+
const result = ymd.validate(input);
|
|
446
|
+
expect(Result.isOk(result)).toBe(true);
|
|
447
|
+
if (Result.isOk(result)) {
|
|
448
|
+
expect(result.value).toBe(input); // ✅ same reference
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test('truthy case - with optional field', () => {
|
|
453
|
+
const x: UnknownRecord = {
|
|
454
|
+
year: 2000,
|
|
455
|
+
month: 12,
|
|
456
|
+
date: 15,
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const result = ymd.validate(x);
|
|
460
|
+
expect(Result.isOk(result)).toBe(true);
|
|
461
|
+
|
|
462
|
+
if (Result.isOk(result)) {
|
|
463
|
+
expect(result.value).toStrictEqual({
|
|
464
|
+
year: 2000,
|
|
465
|
+
month: 12,
|
|
466
|
+
date: 15,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test('falsy case - missing required field', () => {
|
|
472
|
+
const x: UnknownRecord = {
|
|
473
|
+
year: 2000,
|
|
474
|
+
// missing month
|
|
475
|
+
date: 15,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const result = ymd.validate(x);
|
|
479
|
+
expect(Result.isErr(result)).toBe(true);
|
|
480
|
+
|
|
481
|
+
if (Result.isErr(result)) {
|
|
482
|
+
expect(result.value).toStrictEqual([
|
|
483
|
+
{
|
|
484
|
+
path: ['month'],
|
|
485
|
+
actualValue: { year: 2000, date: 15 },
|
|
486
|
+
expectedType:
|
|
487
|
+
'PartiallyRequired<{ year: number, month: number, date: number }, "year" | "month">',
|
|
488
|
+
typeName:
|
|
489
|
+
'PartiallyRequired<{ year: number, month: number, date: number }, "year" | "month">',
|
|
490
|
+
message: 'Missing required key "month"',
|
|
491
|
+
},
|
|
492
|
+
]);
|
|
493
|
+
expect(validationErrorsToMessages(result.value)).toStrictEqual([
|
|
494
|
+
'Missing required key "month" at month',
|
|
495
|
+
]);
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('falsy case - wrong types', () => {
|
|
500
|
+
const x: UnknownRecord = {
|
|
501
|
+
year: 2000,
|
|
502
|
+
month: 'ab',
|
|
503
|
+
date: 'cd',
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const result = ymd.validate(x);
|
|
507
|
+
expect(Result.isErr(result)).toBe(true);
|
|
508
|
+
|
|
509
|
+
if (Result.isErr(result)) {
|
|
510
|
+
expect(result.value).toStrictEqual([
|
|
511
|
+
{
|
|
512
|
+
path: ['month'],
|
|
513
|
+
actualValue: 'ab',
|
|
514
|
+
expectedType: 'number',
|
|
515
|
+
typeName: 'number',
|
|
516
|
+
message: undefined,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
path: ['date'],
|
|
520
|
+
actualValue: 'cd',
|
|
521
|
+
expectedType: 'number',
|
|
522
|
+
typeName: 'number',
|
|
523
|
+
message: undefined,
|
|
524
|
+
},
|
|
525
|
+
]);
|
|
526
|
+
expect(validationErrorsToMessages(result.value)).toStrictEqual([
|
|
527
|
+
'Expected <number> at month, got <string> type value "ab".',
|
|
528
|
+
'Expected <number> at date, got <string> type value "cd".',
|
|
529
|
+
]);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
describe('fill', () => {
|
|
535
|
+
test('from an empty record', () => {
|
|
536
|
+
const x: UnknownRecord = {};
|
|
537
|
+
|
|
538
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
539
|
+
year: 1900,
|
|
540
|
+
month: 1,
|
|
541
|
+
date: 1,
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test('from a filled record', () => {
|
|
546
|
+
const x: UnknownRecord = {
|
|
547
|
+
year: 2000,
|
|
548
|
+
month: 999,
|
|
549
|
+
date: 999,
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
553
|
+
year: 2000,
|
|
554
|
+
month: 999,
|
|
555
|
+
date: 999,
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test('from a partial record - required fields only', () => {
|
|
560
|
+
const x: UnknownRecord = {
|
|
561
|
+
year: 2000,
|
|
562
|
+
month: 12,
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
566
|
+
year: 2000,
|
|
567
|
+
month: 12,
|
|
568
|
+
date: 1,
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test('from a partial record - missing required field', () => {
|
|
573
|
+
const x: UnknownRecord = {
|
|
574
|
+
year: 2000,
|
|
575
|
+
// missing month - fill should provide default
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
579
|
+
year: 2000,
|
|
580
|
+
month: 1,
|
|
581
|
+
date: 1,
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('from a partial record with excess property', () => {
|
|
586
|
+
const x: UnknownRecord = {
|
|
587
|
+
year: 2000,
|
|
588
|
+
month: 12,
|
|
589
|
+
aaaaa: 9999,
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
expect(ymd.fill(x)).toStrictEqual({
|
|
593
|
+
year: 2000,
|
|
594
|
+
month: 12,
|
|
595
|
+
date: 1,
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
});
|
|
600
|
+
});
|