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.
@@ -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';
@@ -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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -5,5 +5,6 @@ export * from './optional.mjs';
5
5
  export * from './partial.mjs';
6
6
  export * from './pick.mjs';
7
7
  export * from './record.mjs';
8
+ export * from './required.mjs';
8
9
  export * from './valueof.mjs';
9
10
  //# sourceMappingURL=index.d.mts.map
@@ -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"}
@@ -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":"MAKa,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;;;;"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-fortress",
3
- "version": "4.2.1",
3
+ "version": "4.3.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -5,4 +5,5 @@ export * from './optional.mjs';
5
5
  export * from './partial.mjs';
6
6
  export * from './pick.mjs';
7
7
  export * from './record.mjs';
8
+ export * from './required.mjs';
8
9
  export * from './valueof.mjs';
@@ -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> => ({
@@ -105,7 +105,7 @@ expectType<
105
105
  }>;
106
106
  allowExcessProperties: boolean;
107
107
  }>
108
- >('~=');
108
+ >('=');
109
109
 
110
110
  expectType<
111
111
  RecordType<
@@ -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
+ });