ts-fortress 2.0.4 → 2.2.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.
Files changed (33) hide show
  1. package/README.md +80 -1
  2. package/dist/index.mjs +1 -1
  3. package/dist/record/index.mjs +1 -1
  4. package/dist/record/key-value-record.d.mts +1 -1
  5. package/dist/record/key-value-record.d.mts.map +1 -1
  6. package/dist/record/record.d.mts +31 -5
  7. package/dist/record/record.d.mts.map +1 -1
  8. package/dist/record/record.mjs +42 -4
  9. package/dist/record/record.mjs.map +1 -1
  10. package/package.json +1 -1
  11. package/src/array/array-of-length.test.mts +16 -1
  12. package/src/array/array.test.mts +16 -1
  13. package/src/array/non-empty-array.test.mts +16 -1
  14. package/src/array/tuple.test.mts +18 -1
  15. package/src/compose/intersection.test.mts +43 -1
  16. package/src/compose/merge-records.test.mts +16 -1
  17. package/src/compose/union.test.mts +16 -1
  18. package/src/enum/enum.test.mts +16 -1
  19. package/src/enum/uint-range.test.mts +14 -1
  20. package/src/primitives/null.test.mts +170 -0
  21. package/src/primitives/number.test.mts +284 -0
  22. package/src/primitives/string.test.mts +341 -0
  23. package/src/primitives/undefined.test.mts +179 -0
  24. package/src/record/key-value-record.mts +4 -4
  25. package/src/record/key-value-record.test.mts +28 -2
  26. package/src/record/keyof.test.mts +12 -0
  27. package/src/record/partial.test.mts +18 -0
  28. package/src/record/record-allow-excess-properties.test.mts +180 -0
  29. package/src/record/record-strict-composition-simple.test.mts +134 -0
  30. package/src/record/record-strict-composition.test.mts +403 -0
  31. package/src/record/record.mts +92 -31
  32. package/src/record/record.test.mts +71 -1
  33. package/src/record/strict-record.test.mts +192 -0
@@ -0,0 +1,341 @@
1
+ import { expectType, Result } from 'ts-data-forge';
2
+ import { type TypeOf } from '../type.mjs';
3
+ import { string, stringLiteral } from './string.mjs';
4
+
5
+ describe('string', () => {
6
+ describe('default value', () => {
7
+ test('without explicit default value', () => {
8
+ const strDefault = string();
9
+ expect(strDefault.defaultValue).toBe('');
10
+ });
11
+
12
+ test('with explicit default value', () => {
13
+ const strCustom = string('hello');
14
+ expect(strCustom.defaultValue).toBe('hello');
15
+ });
16
+ });
17
+
18
+ const str = string('');
19
+ type Str = TypeOf<typeof str>;
20
+
21
+ expectType<Str, string>('=');
22
+ expectType<typeof str.defaultValue, Str>('=');
23
+
24
+ describe('is', () => {
25
+ test('truthy cases', () => {
26
+ const values = [
27
+ '',
28
+ 'hello',
29
+ 'world',
30
+ '123',
31
+ 'true',
32
+ 'false',
33
+ ' ',
34
+ '\n',
35
+ '\t',
36
+ '🎉',
37
+ ];
38
+
39
+ for (const value of values) {
40
+ const v: unknown = value;
41
+
42
+ if (str.is(v)) {
43
+ expectType<typeof v, Str>('=');
44
+ } else {
45
+ expectType<typeof v, unknown>('=');
46
+ }
47
+
48
+ expect(str.is(v)).toBe(true);
49
+ }
50
+ });
51
+
52
+ test('falsy cases', () => {
53
+ const values = [
54
+ 42,
55
+ 0,
56
+ true,
57
+ false,
58
+ null,
59
+ undefined,
60
+ {},
61
+ [],
62
+ Symbol('test'),
63
+ Number.NaN,
64
+ ];
65
+
66
+ for (const value of values) {
67
+ const v: unknown = value;
68
+
69
+ if (str.is(v)) {
70
+ expectType<typeof v, Str>('=');
71
+ } else {
72
+ expectType<typeof v, unknown>('=');
73
+ }
74
+
75
+ expect(str.is(v)).toBe(false);
76
+ }
77
+ });
78
+ });
79
+
80
+ describe('assertIs', () => {
81
+ const assertIs: (a: unknown) => asserts a is string = str.assertIs;
82
+
83
+ test('valid string', () => {
84
+ const value: unknown = 'hello';
85
+ expect(() => {
86
+ assertIs(value);
87
+ }).not.toThrow();
88
+ });
89
+
90
+ test('invalid value throws', () => {
91
+ const value: unknown = 42;
92
+ expect(() => {
93
+ assertIs(value);
94
+ }).toThrow(/Expected string/u);
95
+ });
96
+ });
97
+
98
+ describe('cast', () => {
99
+ test('valid string returns as is', () => {
100
+ const value: unknown = 'hello';
101
+ const result = str.cast(value);
102
+ expect(result).toBe('hello');
103
+ });
104
+
105
+ test('invalid value throws error', () => {
106
+ const value: unknown = 42;
107
+ expect(() => str.cast(value)).toThrow('Expected string, got number');
108
+ });
109
+
110
+ test('throws error with type mismatch', () => {
111
+ const strWithDefault = string('default');
112
+ const value: unknown = 42;
113
+ expect(() => strWithDefault.cast(value)).toThrow(
114
+ 'Expected string, got number',
115
+ );
116
+ });
117
+ });
118
+
119
+ describe('fill', () => {
120
+ test('valid string returns as is', () => {
121
+ const value: unknown = 'hello';
122
+ const result = str.fill(value);
123
+ expect(result).toBe('hello');
124
+ });
125
+
126
+ test('undefined returns default', () => {
127
+ const value: unknown = undefined;
128
+ const result = str.fill(value);
129
+ expect(result).toBe('');
130
+ });
131
+
132
+ test('null returns default', () => {
133
+ const value: unknown = null;
134
+ const result = str.fill(value);
135
+ expect(result).toBe('');
136
+ });
137
+
138
+ test('invalid value returns default', () => {
139
+ const value: unknown = 42;
140
+ const result = str.fill(value);
141
+ expect(result).toBe('');
142
+ });
143
+
144
+ test('uses custom default value for invalid', () => {
145
+ const strWithDefault = string('default');
146
+ const value: unknown = 42;
147
+ const result = strWithDefault.fill(value);
148
+ expect(result).toBe('default');
149
+ });
150
+ });
151
+
152
+ describe('validate', () => {
153
+ test('valid string', () => {
154
+ const value: unknown = 'hello';
155
+ const result = str.validate(value);
156
+
157
+ expect(Result.isOk(result)).toBe(true);
158
+ if (Result.isOk(result)) {
159
+ expect(result.value).toBe('hello');
160
+ }
161
+ });
162
+
163
+ test('empty string is valid', () => {
164
+ const value: unknown = '';
165
+ const result = str.validate(value);
166
+
167
+ expect(Result.isOk(result)).toBe(true);
168
+ if (Result.isOk(result)) {
169
+ expect(result.value).toBe('');
170
+ }
171
+ });
172
+
173
+ test('invalid value', () => {
174
+ const value: unknown = 42;
175
+ const result = str.validate(value);
176
+
177
+ expect(Result.isErr(result)).toBe(true);
178
+ if (Result.isErr(result)) {
179
+ expect(result.value).toStrictEqual([
180
+ {
181
+ path: [],
182
+ actualValue: 42,
183
+ expectedType: 'string',
184
+ typeName: 'string',
185
+ message: undefined,
186
+ },
187
+ ]);
188
+ }
189
+ });
190
+ });
191
+ });
192
+
193
+ describe('stringLiteral', () => {
194
+ const hello = stringLiteral('hello');
195
+ type Hello = TypeOf<typeof hello>;
196
+
197
+ expectType<Hello, 'hello'>('=');
198
+ expectType<typeof hello.defaultValue, Hello>('=');
199
+
200
+ describe('default value', () => {
201
+ test('uses literal as default', () => {
202
+ expect(hello.defaultValue).toBe('hello');
203
+ });
204
+ });
205
+
206
+ describe('is', () => {
207
+ test('exact match returns true', () => {
208
+ const value: unknown = 'hello';
209
+
210
+ if (hello.is(value)) {
211
+ expectType<typeof value, Hello>('=');
212
+ } else {
213
+ expectType<typeof value, unknown>('=');
214
+ }
215
+
216
+ expect(hello.is(value)).toBe(true);
217
+ });
218
+
219
+ test('different string returns false', () => {
220
+ const value: unknown = 'world';
221
+
222
+ if (hello.is(value)) {
223
+ expectType<typeof value, Hello>('=');
224
+ } else {
225
+ expectType<typeof value, unknown>('=');
226
+ }
227
+
228
+ expect(hello.is(value)).toBe(false);
229
+ });
230
+
231
+ test('non-string returns false', () => {
232
+ const value: unknown = 42;
233
+
234
+ expect(hello.is(value)).toBe(false);
235
+ });
236
+ });
237
+
238
+ describe('special string literals', () => {
239
+ test('empty string literal', () => {
240
+ const empty = stringLiteral('');
241
+ type Empty = TypeOf<typeof empty>;
242
+ expectType<Empty, ''>('=');
243
+
244
+ expect(empty.is('')).toBe(true);
245
+ expect(empty.is(' ')).toBe(false);
246
+ });
247
+
248
+ test('whitespace literal', () => {
249
+ const whitespace = stringLiteral(' ');
250
+ type Whitespace = TypeOf<typeof whitespace>;
251
+ expectType<Whitespace, ' '>('=');
252
+
253
+ expect(whitespace.is(' ')).toBe(true);
254
+ expect(whitespace.is(' ')).toBe(false);
255
+ expect(whitespace.is(' ')).toBe(false);
256
+ });
257
+
258
+ test('emoji literal', () => {
259
+ const emoji = stringLiteral('🎉');
260
+ type Emoji = TypeOf<typeof emoji>;
261
+ expectType<Emoji, '🎉'>('=');
262
+
263
+ expect(emoji.is('🎉')).toBe(true);
264
+ expect(emoji.is('🎊')).toBe(false);
265
+ });
266
+
267
+ test('multiline literal', () => {
268
+ const multiline = stringLiteral('line1\nline2');
269
+ type Multiline = TypeOf<typeof multiline>;
270
+ expectType<Multiline, 'line1\nline2'>('=');
271
+
272
+ expect(multiline.is('line1\nline2')).toBe(true);
273
+ expect(multiline.is(String.raw`line1\nline2`)).toBe(false);
274
+ });
275
+ });
276
+
277
+ describe('validate', () => {
278
+ test('exact match', () => {
279
+ const value: unknown = 'hello';
280
+ const result = hello.validate(value);
281
+
282
+ expect(Result.isOk(result)).toBe(true);
283
+ if (Result.isOk(result)) {
284
+ expect(result.value).toBe('hello');
285
+ }
286
+ });
287
+
288
+ test('different value', () => {
289
+ const value: unknown = 'world';
290
+ const result = hello.validate(value);
291
+
292
+ expect(Result.isErr(result)).toBe(true);
293
+ if (Result.isErr(result)) {
294
+ expect(result.value).toStrictEqual([
295
+ {
296
+ path: [],
297
+ actualValue: 'world',
298
+ expectedType: 'stringLiteral(hello)',
299
+ typeName: 'stringLiteral(hello)',
300
+ message: undefined,
301
+ },
302
+ ]);
303
+ }
304
+ });
305
+ });
306
+
307
+ describe('cast', () => {
308
+ test('valid literal returns as is', () => {
309
+ const value: unknown = 'hello';
310
+ const result = hello.cast(value);
311
+ expect(result).toBe('hello');
312
+ });
313
+
314
+ test('invalid value throws error', () => {
315
+ const value: unknown = 'world';
316
+ expect(() => hello.cast(value)).toThrow(
317
+ 'Expected stringLiteral(hello), got string',
318
+ );
319
+ });
320
+ });
321
+
322
+ describe('fill', () => {
323
+ test('valid literal returns as is', () => {
324
+ const value: unknown = 'hello';
325
+ const result = hello.fill(value);
326
+ expect(result).toBe('hello');
327
+ });
328
+
329
+ test('undefined returns default (the literal itself)', () => {
330
+ const value: unknown = undefined;
331
+ const result = hello.fill(value);
332
+ expect(result).toBe('hello');
333
+ });
334
+
335
+ test('different value returns default', () => {
336
+ const value: unknown = 'world';
337
+ const result = hello.fill(value);
338
+ expect(result).toBe('hello');
339
+ });
340
+ });
341
+ });
@@ -0,0 +1,179 @@
1
+ /* eslint-disable @typescript-eslint/no-confusing-void-expression */
2
+ import { expectType, Result } from 'ts-data-forge';
3
+ import { undefinedType } from './undefined.mjs';
4
+
5
+ describe('undefinedType', () => {
6
+ expectType<undefined, undefined>('=');
7
+ expectType<typeof undefinedType.defaultValue, undefined>('=');
8
+
9
+ describe('default value', () => {
10
+ test('is undefined', () => {
11
+ expect(undefinedType.defaultValue).toBeUndefined();
12
+ });
13
+ });
14
+
15
+ describe('is', () => {
16
+ test('undefined returns true', () => {
17
+ const value: unknown = undefined;
18
+
19
+ if (undefinedType.is(value)) {
20
+ expectType<typeof value, undefined>('=');
21
+ } else {
22
+ expectType<typeof value, {} | null>('=');
23
+ }
24
+
25
+ expect(undefinedType.is(value)).toBe(true);
26
+ });
27
+
28
+ test('null returns false', () => {
29
+ const value: unknown = null;
30
+
31
+ if (undefinedType.is(value)) {
32
+ expectType<typeof value, undefined>('=');
33
+ } else {
34
+ expectType<typeof value, {} | null>('=');
35
+ }
36
+
37
+ expect(undefinedType.is(value)).toBe(false);
38
+ });
39
+
40
+ test('other values return false', () => {
41
+ const values = [
42
+ 0,
43
+ '',
44
+ false,
45
+ Number.NaN,
46
+ {},
47
+ [],
48
+ 'undefined',
49
+ 42,
50
+ true,
51
+ null,
52
+ ];
53
+
54
+ for (const value of values) {
55
+ const v: unknown = value;
56
+
57
+ if (undefinedType.is(v)) {
58
+ expectType<typeof v, undefined>('=');
59
+ } else {
60
+ expectType<typeof v, {} | null>('=');
61
+ }
62
+
63
+ expect(undefinedType.is(v)).toBe(false);
64
+ }
65
+ });
66
+ });
67
+
68
+ describe('assertIs', () => {
69
+ test('undefined does not throw', () => {
70
+ const value: unknown = undefined;
71
+ expect(() => {
72
+ undefinedType.assertIs(value);
73
+ }).not.toThrow();
74
+ });
75
+
76
+ test('non-undefined throws', () => {
77
+ const value: unknown = null;
78
+ expect(() => {
79
+ undefinedType.assertIs(value);
80
+ }).toThrow(`Expected undefined, got ${typeof value}`);
81
+ });
82
+ });
83
+
84
+ describe('cast', () => {
85
+ test('undefined returns undefined', () => {
86
+ const value: unknown = undefined;
87
+ const result = undefinedType.cast(value);
88
+ expect(result).toBeUndefined();
89
+ });
90
+
91
+ test('non-undefined throws error', () => {
92
+ const value: unknown = null;
93
+ expect(() => {
94
+ undefinedType.cast(value);
95
+ }).toThrow('Expected undefined, got object');
96
+ });
97
+
98
+ test('any non-undefined value throws error', () => {
99
+ const values = [0, '', false, null, 42, 'hello'];
100
+
101
+ for (const value of values) {
102
+ expect(() => {
103
+ undefinedType.cast(value);
104
+ }).toThrow(`Expected undefined, got ${typeof value}`);
105
+ }
106
+ });
107
+ });
108
+
109
+ describe('fill', () => {
110
+ test('undefined returns undefined', () => {
111
+ const value: unknown = undefined;
112
+ const result = undefinedType.fill(value);
113
+ expect(result).toBeUndefined();
114
+ });
115
+
116
+ test('null returns default (undefined)', () => {
117
+ const value: unknown = null;
118
+ const result = undefinedType.fill(value);
119
+ expect(result).toBeUndefined();
120
+ });
121
+
122
+ test('other values return default (undefined)', () => {
123
+ const values = [0, '', false, 42, 'hello', {}];
124
+
125
+ for (const value of values) {
126
+ const result = undefinedType.fill(value);
127
+ expect(result).toBeUndefined();
128
+ }
129
+ });
130
+ });
131
+
132
+ describe('validate', () => {
133
+ test('undefined is valid', () => {
134
+ const value: unknown = undefined;
135
+ const result = undefinedType.validate(value);
136
+
137
+ expect(Result.isOk(result)).toBe(true);
138
+ if (Result.isOk(result)) {
139
+ expect(result.value).toBeUndefined();
140
+ }
141
+ });
142
+
143
+ test('null is invalid', () => {
144
+ const value: unknown = null;
145
+ const result = undefinedType.validate(value);
146
+
147
+ expect(Result.isErr(result)).toBe(true);
148
+ if (Result.isErr(result)) {
149
+ expect(result.value).toStrictEqual([
150
+ {
151
+ path: [],
152
+ actualValue: null,
153
+ expectedType: 'undefined',
154
+ typeName: 'undefined',
155
+ message: undefined,
156
+ },
157
+ ]);
158
+ }
159
+ });
160
+
161
+ test('other values are invalid', () => {
162
+ const value: unknown = 0;
163
+ const result = undefinedType.validate(value);
164
+
165
+ expect(Result.isErr(result)).toBe(true);
166
+ if (Result.isErr(result)) {
167
+ expect(result.value).toStrictEqual([
168
+ {
169
+ path: [],
170
+ actualValue: 0,
171
+ expectedType: 'undefined',
172
+ typeName: 'undefined',
173
+ message: undefined,
174
+ },
175
+ ]);
176
+ }
177
+ });
178
+ });
179
+ });
@@ -8,10 +8,10 @@ import {
8
8
  type ValidationErrorWithMessage,
9
9
  } from '../validation-error.mjs';
10
10
 
11
- type RecordResultType<K extends Type<string>, V extends Type<unknown>> = Record<
12
- TypeOf<K>,
13
- TypeOf<V>
14
- >;
11
+ type RecordResultType<
12
+ K extends Type<string>,
13
+ V extends Type<unknown>,
14
+ > = ReadonlyRecord<TypeOf<K>, TypeOf<V>>;
15
15
 
16
16
  export const keyValueRecord = <K extends Type<string>, V extends Type<unknown>>(
17
17
  keyType: K,
@@ -1,7 +1,10 @@
1
1
  import { expectType, Result } from 'ts-data-forge';
2
2
  import { number, string } from '../primitives/index.mjs';
3
3
  import { type TypeOf } from '../type.mjs';
4
- import { validationErrorsToMessages } from '../validation-error.mjs';
4
+ import {
5
+ type ValidationError,
6
+ validationErrorsToMessages,
7
+ } from '../validation-error.mjs';
5
8
  import { keyValueRecord } from './key-value-record.mjs';
6
9
 
7
10
  describe('keyValueRecord', () => {
@@ -9,7 +12,7 @@ describe('keyValueRecord', () => {
9
12
 
10
13
  type StrNumRecord = TypeOf<typeof strNumRecord>;
11
14
 
12
- expectType<StrNumRecord, Record<string, number>>('=');
15
+ expectType<StrNumRecord, ReadonlyRecord<string, number>>('=');
13
16
 
14
17
  expectType<typeof strNumRecord.defaultValue, StrNumRecord>('=');
15
18
 
@@ -48,6 +51,29 @@ describe('keyValueRecord', () => {
48
51
  });
49
52
 
50
53
  describe('validate', () => {
54
+ test('truthy case', () => {
55
+ const x: UnknownRecord = {
56
+ year: 2000,
57
+ month: 12,
58
+ date: 25,
59
+ };
60
+
61
+ const result = strNumRecord.validate(x);
62
+ expectType<
63
+ typeof result,
64
+ Result<StrNumRecord, readonly ValidationError[]>
65
+ >('=');
66
+ expect(Result.isOk(result)).toBe(true);
67
+
68
+ if (Result.isOk(result)) {
69
+ expect(result.value).toStrictEqual({
70
+ year: 2000,
71
+ month: 12,
72
+ date: 25,
73
+ });
74
+ }
75
+ });
76
+
51
77
  test('falsy case', () => {
52
78
  const x: UnknownRecord = {
53
79
  year: 2000,
@@ -49,6 +49,18 @@ describe('keyof', () => {
49
49
  });
50
50
 
51
51
  describe('validate', () => {
52
+ test('truthy case', () => {
53
+ const x: unknown = 'year';
54
+
55
+ const result = ymdKey.validate(x);
56
+ expect(Result.isOk(result)).toBe(true);
57
+
58
+ if (Result.isOk(result)) {
59
+ expectType<typeof result.value, Ymd>('=');
60
+ expect(result.value).toBe('year');
61
+ }
62
+ });
63
+
52
64
  test('falsy case', () => {
53
65
  const x: unknown = 'minutes';
54
66
 
@@ -269,6 +269,24 @@ describe('partial', () => {
269
269
  });
270
270
 
271
271
  describe('validate', () => {
272
+ test('truthy case', () => {
273
+ const x: UnknownRecord = {
274
+ year: 2000,
275
+ month: 12,
276
+ };
277
+
278
+ const result = ymd.validate(x);
279
+ expectType<typeof result, Result<Ymd, readonly ValidationError[]>>('=');
280
+ expect(Result.isOk(result)).toBe(true);
281
+
282
+ if (Result.isOk(result)) {
283
+ expect(result.value).toStrictEqual({
284
+ year: 2000,
285
+ month: 12,
286
+ });
287
+ }
288
+ });
289
+
272
290
  test('falsy case', () => {
273
291
  const x: UnknownRecord = {
274
292
  year: 2000,