ts-fortress 4.0.0 → 4.1.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 { valueof } from './record/valueof.mjs';
55
56
  export { createAssertFn } from './utils/create-assert-fn.mjs';
56
57
  export { createCastFn } from './utils/create-cast-fn.mjs';
57
58
  export { createIsFn } from './utils/create-is-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 { valueof } from './record/valueof.mjs';
54
55
  import 'ts-data-forge';
55
56
  export { createAssertFn } from './utils/create-assert-fn.mjs';
56
57
  export { createCastFn } from './utils/create-cast-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,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 './valueof.mjs';
8
9
  //# 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"}
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"}
@@ -5,4 +5,5 @@ 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 { valueof } from './valueof.mjs';
8
9
  //# 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":";;;;;;;"}
@@ -0,0 +1,8 @@
1
+ import { type RecordType, type Type } from '../type.mjs';
2
+ export declare const valueof: <const R extends ReadonlyRecord<string, Type<unknown>>>(recordType: RecordType<R>, options?: Partial<Readonly<{
3
+ typeName: string;
4
+ }>>) => ValueOfType<R>;
5
+ type ValueofTypeSub<R extends ReadonlyRecord<string, Type<unknown>>> = Type<R[keyof R]['defaultValue']>;
6
+ type ValueOfType<R extends ReadonlyRecord<string, Type<unknown>>> = IsNever<keyof R> extends true ? Type<undefined> : ValueofTypeSub<R>;
7
+ export {};
8
+ //# sourceMappingURL=valueof.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"valueof.d.mts","sourceRoot":"","sources":["../../src/record/valueof.mts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAE,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAEzD,eAAO,MAAM,OAAO,GAAI,KAAK,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,EAC3E,YAAY,UAAU,CAAC,CAAC,CAAC,EACzB,UAAU,OAAO,CACf,QAAQ,CAAC;IACP,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC,CACH,KACA,WAAW,CAAC,CAAC,CAiBf,CAAC;AAEF,KAAK,cAAc,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,IAAI,CACzE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,cAAc,CAAC,CAC3B,CAAC;AAEF,KAAK,WAAW,CAAC,CAAC,SAAS,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,IAC9D,OAAO,CAAC,MAAM,CAAC,CAAC,SAAS,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { Arr } from 'ts-data-forge';
2
+ import { union } from '../compose/union.mjs';
3
+ import { undefinedType } from '../primitives/undefined.mjs';
4
+
5
+ const valueof = (recordType, options) => {
6
+ const types = Object.values(recordType.shape);
7
+ if (Arr.isArrayAtLeastLength(types, 2)) {
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
9
+ return union(types, {
10
+ typeName: options?.typeName ?? `ValueOf<${recordType.typeName}>`,
11
+ });
12
+ }
13
+ if (Arr.isNonEmpty(types)) {
14
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
15
+ return types[0];
16
+ }
17
+ // types is empty
18
+ return undefinedType;
19
+ };
20
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
21
+
22
+ export { valueof };
23
+ //# sourceMappingURL=valueof.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"valueof.mjs","sources":["../../src/record/valueof.mts"],"sourcesContent":[null],"names":[],"mappings":";;;;MAKa,OAAO,GAAG,CACrB,UAAyB,EACzB,OAIC,KACiB;IAClB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC;IAE7C,IAAI,GAAG,CAAC,oBAAoB,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE;;QAEtC,OAAO,KAAK,CAAC,KAAK,EAAE;YAClB,QAAQ,EAAE,OAAO,EAAE,QAAQ,IAAI,CAAA,QAAA,EAAW,UAAU,CAAC,QAAQ,CAAA,CAAA,CAAG;AACjE,SAAA,CAA+C;;AAGlD,IAAA,IAAI,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE;;AAEzB,QAAA,OAAO,KAAK,CAAC,CAAC,CAA+C;;;AAI/D,IAAA,OAAO,aAAsC;AAC/C;AASA;;;;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-fortress",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -5,3 +5,4 @@ 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 './valueof.mjs';
@@ -0,0 +1,57 @@
1
+ import { Arr, expectType } from 'ts-data-forge';
2
+ import { union } from '../compose/index.mjs';
3
+ import { undefinedType } from '../primitives/index.mjs';
4
+ import { type RecordType, type Type } from '../type.mjs';
5
+
6
+ export const valueof = <const R extends ReadonlyRecord<string, Type<unknown>>>(
7
+ recordType: RecordType<R>,
8
+ options?: Partial<
9
+ Readonly<{
10
+ typeName: string;
11
+ }>
12
+ >,
13
+ ): ValueOfType<R> => {
14
+ const types = Object.values(recordType.shape);
15
+
16
+ if (Arr.isArrayAtLeastLength(types, 2)) {
17
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
18
+ return union(types, {
19
+ typeName: options?.typeName ?? `ValueOf<${recordType.typeName}>`,
20
+ }) satisfies ValueofTypeSub<R> as ValueOfType<R>;
21
+ }
22
+
23
+ if (Arr.isNonEmpty(types)) {
24
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
25
+ return types[0] satisfies ValueofTypeSub<R> as ValueOfType<R>;
26
+ }
27
+
28
+ // types is empty
29
+ return undefinedType satisfies ValueOfType<R>;
30
+ };
31
+
32
+ type ValueofTypeSub<R extends ReadonlyRecord<string, Type<unknown>>> = Type<
33
+ R[keyof R]['defaultValue']
34
+ >;
35
+
36
+ type ValueOfType<R extends ReadonlyRecord<string, Type<unknown>>> =
37
+ IsNever<keyof R> extends true ? Type<undefined> : ValueofTypeSub<R>;
38
+
39
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
40
+ expectType<ValueOfType<{}>, Type<undefined>>('=');
41
+
42
+ expectType<
43
+ ValueOfType<{ a: Type<0>; b: Type<1>; c: Type<2> }>,
44
+ Type<0 | 1 | 2>
45
+ >('=');
46
+
47
+ expectType<
48
+ ValueOfType<{ x: Type<string>; y: Type<number>; z: Type<boolean> }>,
49
+ Type<string | number | boolean>
50
+ >('=');
51
+
52
+ expectType<
53
+ ValueOfType<{ same: Type<string>; value: Type<string> }>,
54
+ Type<string>
55
+ >('=');
56
+
57
+ expectType<ValueOfType<{ never: Type<never> }>, Type<never>>('=');
@@ -0,0 +1,178 @@
1
+ import { expectType, Result } from 'ts-data-forge';
2
+ import { number, string } from '../primitives/index.mjs';
3
+ import { type Type, type TypeOf } from '../type.mjs';
4
+ import { validationErrorsToMessages } from '../utils/index.mjs';
5
+ import { record } from './record.mjs';
6
+ import { valueof } from './valueof.mjs';
7
+
8
+ describe('valueof', () => {
9
+ const ymdValue = valueof(
10
+ record({
11
+ year: number(1900),
12
+ month: number(1),
13
+ date: number(1),
14
+ }),
15
+ );
16
+
17
+ expectType<typeof ymdValue, Type<number>>('=');
18
+
19
+ type YmdValue = TypeOf<typeof ymdValue>;
20
+
21
+ expectType<YmdValue, number>('=');
22
+
23
+ expectType<typeof ymdValue.defaultValue, YmdValue>('=');
24
+
25
+ describe('is', () => {
26
+ test('truthy case', () => {
27
+ const x: unknown = 2000;
28
+
29
+ if (ymdValue.is(x)) {
30
+ expectType<typeof x, number>('=');
31
+ } else {
32
+ expectType<typeof x, unknown>('=');
33
+ }
34
+
35
+ expect(ymdValue.is(x)).toBe(true);
36
+ });
37
+
38
+ test('falsy case', () => {
39
+ const x: unknown = 'not-a-number';
40
+
41
+ if (ymdValue.is(x)) {
42
+ expectType<typeof x, number>('=');
43
+ } else {
44
+ expectType<typeof x, unknown>('=');
45
+ }
46
+
47
+ expect(ymdValue.is(x)).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe('validate', () => {
52
+ test('truthy case', () => {
53
+ const x: unknown = 2000;
54
+
55
+ const result = ymdValue.validate(x);
56
+ expect(Result.isOk(result)).toBe(true);
57
+
58
+ if (Result.isOk(result)) {
59
+ expectType<typeof result.value, YmdValue>('=');
60
+ expect(result.value).toBe(2000);
61
+ }
62
+ });
63
+
64
+ test('validate returns input as-is for OK cases', () => {
65
+ const input = 2000;
66
+ const result = ymdValue.validate(input);
67
+ expect(Result.isOk(result)).toBe(true);
68
+ if (Result.isOk(result)) {
69
+ expect(result.value).toBe(input); // ✅ same reference
70
+ }
71
+ });
72
+
73
+ test('falsy case', () => {
74
+ const x: unknown = 'not-a-number';
75
+
76
+ const result = ymdValue.validate(x);
77
+ expect(Result.isErr(result)).toBe(true);
78
+
79
+ if (Result.isErr(result)) {
80
+ expect(result.value).toStrictEqual([
81
+ {
82
+ path: [],
83
+ actualValue: 'not-a-number',
84
+ expectedType:
85
+ 'ValueOf<{ year: number, month: number, date: number }>',
86
+ typeName: 'ValueOf<{ year: number, month: number, date: number }>',
87
+ message:
88
+ 'The type of value is expected to be one of the elements contained in { number, number, number }',
89
+ },
90
+ ]);
91
+ expect(validationErrorsToMessages(result.value)).toStrictEqual([
92
+ 'The type of value is expected to be one of the elements contained in { number, number, number }',
93
+ ]);
94
+ }
95
+ });
96
+ });
97
+
98
+ describe('fill', () => {
99
+ test('from external value', () => {
100
+ const x: unknown = 'not-a-number';
101
+
102
+ expect(ymdValue.fill(x)).toBe(1900);
103
+ });
104
+
105
+ test('from valid number', () => {
106
+ const x = 2000;
107
+
108
+ expect(ymdValue.fill(x)).toBe(2000);
109
+ });
110
+ });
111
+
112
+ describe('mixed types', () => {
113
+ const mixedValue = valueof(
114
+ record({
115
+ name: string('John'),
116
+ age: number(25),
117
+ }),
118
+ );
119
+
120
+ expectType<typeof mixedValue, Type<string | number>>('=');
121
+
122
+ type MixedValue = TypeOf<typeof mixedValue>;
123
+
124
+ expectType<MixedValue, string | number>('=');
125
+
126
+ test('accepts string', () => {
127
+ const x: unknown = 'Alice';
128
+ expect(mixedValue.is(x)).toBe(true);
129
+ });
130
+
131
+ test('accepts number', () => {
132
+ const x: unknown = 30;
133
+ expect(mixedValue.is(x)).toBe(true);
134
+ });
135
+
136
+ test('rejects boolean', () => {
137
+ const x: unknown = true;
138
+ expect(mixedValue.is(x)).toBe(false);
139
+ });
140
+
141
+ test('validate mixed values', () => {
142
+ const stringResult = mixedValue.validate('Alice');
143
+ expect(Result.isOk(stringResult)).toBe(true);
144
+ if (Result.isOk(stringResult)) {
145
+ expect(stringResult.value).toBe('Alice');
146
+ }
147
+
148
+ const numberResult = mixedValue.validate(30);
149
+ expect(Result.isOk(numberResult)).toBe(true);
150
+ if (Result.isOk(numberResult)) {
151
+ expect(numberResult.value).toBe(30);
152
+ }
153
+ });
154
+
155
+ test('fill from invalid value uses first type default', () => {
156
+ expect(mixedValue.fill(true)).toBe('John');
157
+ });
158
+ });
159
+
160
+ describe('same type values', () => {
161
+ const sameTypeValue = valueof(
162
+ record({
163
+ first: string('hello'),
164
+ second: string('world'),
165
+ }),
166
+ );
167
+
168
+ expectType<typeof sameTypeValue, Type<string>>('=');
169
+
170
+ test('accepts any string', () => {
171
+ expect(sameTypeValue.is('test')).toBe(true);
172
+ });
173
+
174
+ test('rejects non-string', () => {
175
+ expect(sameTypeValue.is(123)).toBe(false);
176
+ });
177
+ });
178
+ });