ts-fortress 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,10 +13,9 @@ TypeScript-first schema validation library with static type inference.
|
|
|
13
13
|
|
|
14
14
|
- 🔒 **Type-safe validation** - Full TypeScript support with static type inference
|
|
15
15
|
- 🏗️ **Composable schemas** - Build complex validation schemas from simple building blocks
|
|
16
|
-
- 🎯 **Zero-runtime overhead** - Type information is inferred at compile time
|
|
17
16
|
- 🔄 **Result-based error handling** - Structured error reporting with `Result<T, readonly ValidationError[]>`
|
|
18
17
|
- 🏷️ **Branded types** - Rich collection of branded number types (Int, SafeInt, PositiveInt, etc.)
|
|
19
|
-
- ⚡ **Performance focused** -
|
|
18
|
+
- ⚡ **Performance focused** - Optimized validation with minimal runtime overhead (negligible impact on application performance)
|
|
20
19
|
- 🧩 **Functional composition** - Union, intersection, and merge operations for schema composition
|
|
21
20
|
- 🛠️ **Required default values** - All schemas require explicit default values, enabling automatic data filling via `fill()` function
|
|
22
21
|
|
|
@@ -50,7 +49,13 @@ const User = t.record({
|
|
|
50
49
|
|
|
51
50
|
// Infer TypeScript type
|
|
52
51
|
type User = t.TypeOf<typeof User>;
|
|
53
|
-
// ↑ {
|
|
52
|
+
// ↑ Readonly<{
|
|
53
|
+
// id: string;
|
|
54
|
+
// name: string;
|
|
55
|
+
// age: number;
|
|
56
|
+
// email: string;
|
|
57
|
+
// isActive: boolean
|
|
58
|
+
// }>
|
|
54
59
|
|
|
55
60
|
// Validate data
|
|
56
61
|
const userData = {
|
|
@@ -66,12 +71,14 @@ if (User.is(userData)) {
|
|
|
66
71
|
console.log(`User: ${userData.name}, Age: ${userData.age}`);
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
import { Result } from 'ts-data-forge';
|
|
75
|
+
|
|
69
76
|
// Get validation result with error details
|
|
70
77
|
const result = User.validate(userData);
|
|
71
|
-
if (
|
|
78
|
+
if (Result.isOk(result)) {
|
|
72
79
|
const user = result.value; // typed as User
|
|
73
80
|
} else {
|
|
74
|
-
console.error('Validation errors:', result.value); //
|
|
81
|
+
console.error('Validation errors:', result.value); // readonly ValidationError[] showing error information
|
|
75
82
|
}
|
|
76
83
|
```
|
|
77
84
|
|
|
@@ -120,6 +127,14 @@ console.log(filledData);
|
|
|
120
127
|
// fill() is type-safe and always returns a complete object
|
|
121
128
|
type UserProfile = t.TypeOf<typeof UserProfile>;
|
|
122
129
|
const completeUser: UserProfile = UserProfile.fill(anyPartialData);
|
|
130
|
+
|
|
131
|
+
// Important: Default value filling only occurs when fill() is called
|
|
132
|
+
// The is() and validate() functions can still detect missing keys
|
|
133
|
+
console.log(UserProfile.is(partialData)); // false - missing required keys
|
|
134
|
+
const result = UserProfile.validate(partialData);
|
|
135
|
+
if (Result.isErr(result)) {
|
|
136
|
+
console.log(result.value); // ValidationError[] showing missing keys
|
|
137
|
+
}
|
|
123
138
|
```
|
|
124
139
|
|
|
125
140
|
### Benefits of Required Default Values
|
|
@@ -169,7 +184,7 @@ export const SomeObject = z.object({
|
|
|
169
184
|
key2: 'string', // Should be z.string()
|
|
170
185
|
});
|
|
171
186
|
|
|
172
|
-
export type SomeObject = z.infer<typeof SomeObject>;
|
|
187
|
+
export type SomeObject = z.infer<typeof SomeObject>; // inferred as { key1: unknown, key2: unknown }
|
|
173
188
|
```
|
|
174
189
|
|
|
175
190
|
The above Zod schema will fail at runtime because raw values (`1`, `'string'`) are not valid Zod validators.
|
|
@@ -282,7 +297,6 @@ user.tags[0] = 'modified'; // No error
|
|
|
282
297
|
- **Immutability by default**: Prevents accidental mutations that can lead to bugs
|
|
283
298
|
- **Functional programming support**: Encourages functional programming patterns
|
|
284
299
|
- **Predictable data flow**: Ensures data integrity throughout your application
|
|
285
|
-
- **Better optimization**: Enables better compiler optimizations and reasoning about code
|
|
286
300
|
- **Thread safety**: Immutable data is inherently safe to share between contexts
|
|
287
301
|
|
|
288
302
|
### Runtime-Type Consistency Issues in io-ts
|
|
@@ -399,7 +413,7 @@ const C = t.partial(
|
|
|
399
413
|
const UnionBA = t.union([B, A]);
|
|
400
414
|
const result = UnionBA.validate({ A: 1 });
|
|
401
415
|
|
|
402
|
-
if (
|
|
416
|
+
if (Result.isOk(result)) {
|
|
403
417
|
console.log(result.value); // { A: 1 } <- ✅ Correct! No unexpected fields
|
|
404
418
|
console.log(A.is(result.value)); // true <- ✅ Correct
|
|
405
419
|
console.log(B.is(result.value)); // false <- ✅ Correct! B requires field B
|
|
@@ -411,7 +425,7 @@ const C = t.partial(
|
|
|
411
425
|
const UnionCA = t.union([C, A]);
|
|
412
426
|
const result = UnionCA.validate({ A: 1 });
|
|
413
427
|
|
|
414
|
-
if (
|
|
428
|
+
if (Result.isOk(result)) {
|
|
415
429
|
console.log(result.value); // { A: 1 } <- ✅ Correct and consistent
|
|
416
430
|
console.log(A.is(result.value)); // true <- ✅ Correct
|
|
417
431
|
console.log(C.is(result.value)); // true <- ✅ Consistent! ts-fortress partial types allow extra fields
|
|
@@ -421,7 +435,7 @@ const C = t.partial(
|
|
|
421
435
|
|
|
422
436
|
This makes ts-fortress especially valuable in large codebases where schema correctness is critical and runtime failures need to be minimized.
|
|
423
437
|
|
|
424
|
-
|
|
438
|
+
### Migration from io-ts
|
|
425
439
|
|
|
426
440
|
If you're coming from io-ts, here's how common patterns translate:
|
|
427
441
|
|
|
@@ -453,7 +467,7 @@ Key differences:
|
|
|
453
467
|
|
|
454
468
|
- **Default values**: ts-fortress requires explicit default values for better type safety
|
|
455
469
|
- **Naming**: `record` instead of `type`, more explicit function names
|
|
456
|
-
- **Error handling**:
|
|
470
|
+
- **Error handling**: `Result` type instead of `Either`
|
|
457
471
|
- **Branded types**: Rich collection of pre-built branded number types
|
|
458
472
|
|
|
459
473
|
## Core Concepts
|
|
@@ -548,7 +562,7 @@ const UInt16 = t.uint16(0);
|
|
|
548
562
|
|
|
549
563
|
// Usage
|
|
550
564
|
const userIdResult = UserId.validate('user_123');
|
|
551
|
-
if (
|
|
565
|
+
if (Result.isOk(userIdResult)) {
|
|
552
566
|
const id: UserId = userIdResult.value;
|
|
553
567
|
}
|
|
554
568
|
```
|
|
@@ -589,11 +603,15 @@ const ExtendedUserType = t.mergeRecords([
|
|
|
589
603
|
|
|
590
604
|
```typescript
|
|
591
605
|
// String enums
|
|
592
|
-
const ColorEnum = t.
|
|
606
|
+
const ColorEnum = t.enumType(['red', 'green', 'blue']);
|
|
593
607
|
type Color = t.TypeOf<typeof ColorEnum>; // 'red' | 'green' | 'blue'
|
|
594
608
|
|
|
595
609
|
// Numeric ranges
|
|
596
|
-
const DiceRoll = t.uintRange(
|
|
610
|
+
const DiceRoll = t.uintRange({
|
|
611
|
+
start: 1,
|
|
612
|
+
end: 7,
|
|
613
|
+
defaultValue: 1,
|
|
614
|
+
}); // integers from 1 to 6
|
|
597
615
|
type DiceRoll = t.TypeOf<typeof DiceRoll>; // 1 | 2 | 3 | 4 | 5 | 6
|
|
598
616
|
```
|
|
599
617
|
|
|
@@ -612,14 +630,14 @@ const invalidData = { name: 123, age: 'not a number' };
|
|
|
612
630
|
const result = UserType.validate(invalidData);
|
|
613
631
|
if (Result.isErr(result)) {
|
|
614
632
|
// result.value is an array of ValidationError objects
|
|
615
|
-
result.value
|
|
633
|
+
for (const error of result.value) {
|
|
616
634
|
console.log('Path:', error.path); // ['name'] or ['age']
|
|
617
635
|
console.log('Expected:', error.expectedType); // 'string' or 'number'
|
|
618
636
|
console.log('Actual:', error.actualValue); // 123 or 'not a number'
|
|
619
637
|
console.log('Type:', error.typeName); // type being validated
|
|
620
|
-
}
|
|
638
|
+
}
|
|
621
639
|
|
|
622
|
-
// Convert to
|
|
640
|
+
// Convert to string messages
|
|
623
641
|
const messages = t.validationErrorsToMessages(result.value);
|
|
624
642
|
console.log(messages);
|
|
625
643
|
// ['Expected string at name, got number', 'Expected number at age, got string']
|
|
@@ -638,34 +656,13 @@ try {
|
|
|
638
656
|
Each validation error provides detailed information:
|
|
639
657
|
|
|
640
658
|
```typescript
|
|
641
|
-
|
|
642
|
-
path: readonly string[];
|
|
659
|
+
type ValidationError = Readonly<{
|
|
660
|
+
path: readonly string[];
|
|
643
661
|
actualValue: unknown; // The actual value that failed validation
|
|
644
662
|
expectedType: string; // The expected type or constraint
|
|
645
|
-
message
|
|
663
|
+
message: string | undefined; // Optional custom error message
|
|
646
664
|
typeName: string; // Name of the type being validated
|
|
647
|
-
}
|
|
648
|
-
```
|
|
649
|
-
|
|
650
|
-
### Error Utilities
|
|
651
|
-
|
|
652
|
-
```typescript
|
|
653
|
-
import * as t from 'ts-fortress';
|
|
654
|
-
|
|
655
|
-
// Convert ValidationError objects to string messages
|
|
656
|
-
const messages = t.validationErrorsToMessages(errors);
|
|
657
|
-
|
|
658
|
-
// Convert single ValidationError to message
|
|
659
|
-
const message = t.validationErrorToMessage(error);
|
|
660
|
-
|
|
661
|
-
// Create custom validation errors
|
|
662
|
-
const customError = t.createValidationError({
|
|
663
|
-
path: ['user', 'email'],
|
|
664
|
-
actualValue: 'invalid-email',
|
|
665
|
-
expectedType: 'valid email address',
|
|
666
|
-
typeName: 'EmailAddress',
|
|
667
|
-
message: 'Please provide a valid email address',
|
|
668
|
-
});
|
|
665
|
+
}>;
|
|
669
666
|
```
|
|
670
667
|
|
|
671
668
|
## API Reference
|
|
@@ -680,23 +677,25 @@ const customError = t.createValidationError({
|
|
|
680
677
|
|
|
681
678
|
### Collections
|
|
682
679
|
|
|
683
|
-
- `t.array(elementType
|
|
684
|
-
- `t.nonEmptyArray(elementType
|
|
685
|
-
- `t.tuple([
|
|
686
|
-
- `t.arrayOfLength(
|
|
680
|
+
- `t.array(elementType)` - Array validation
|
|
681
|
+
- `t.nonEmptyArray(elementType)` - Non-empty array validation
|
|
682
|
+
- `t.tuple([t1, t2, ..., tN])` - Fixed-length tuple validation
|
|
683
|
+
- `t.arrayOfLength(size, elementType)` - Fixed-length array validation
|
|
687
684
|
|
|
688
685
|
### Objects
|
|
689
686
|
|
|
690
|
-
- `t.record(schema
|
|
687
|
+
- `t.record(schema)` - Object validation
|
|
688
|
+
- `t.keyValueRecord(keyType, valueType)` - Corresponding to the `Record<K, V>` type
|
|
691
689
|
- `t.partial(recordType)` - Make all fields optional
|
|
692
690
|
- `t.optional(type)` - Optional field wrapper
|
|
693
691
|
- `t.pick(recordType, keys)` - Pick specific fields
|
|
694
692
|
- `t.omit(recordType, keys)` - Omit specific fields
|
|
693
|
+
- `t.keyof(recordType)` - Key of the record type.
|
|
695
694
|
|
|
696
695
|
### Composition
|
|
697
696
|
|
|
698
|
-
- `t.union(types
|
|
699
|
-
- `t.intersection(types)` - Intersection type validation
|
|
697
|
+
- `t.union(types)` - Union type validation
|
|
698
|
+
- `t.intersection(types, defaultType)` - Intersection type validation
|
|
700
699
|
- `t.mergeRecords(recordTypes)` - Merge multiple record types
|
|
701
700
|
|
|
702
701
|
### Branded Types
|
|
@@ -708,8 +707,9 @@ const customError = t.createValidationError({
|
|
|
708
707
|
### Utilities
|
|
709
708
|
|
|
710
709
|
- `t.TypeOf<T>` - Extract TypeScript type from validator
|
|
711
|
-
- `t.
|
|
712
|
-
- `t.uintRange(
|
|
710
|
+
- `t.enumType(values)` - Enum validation
|
|
711
|
+
- `t.uintRange({ start, end, defaultValue? })` - Integer range validation
|
|
712
|
+
- `t.unknown` - Unknown Type
|
|
713
713
|
|
|
714
714
|
## Contributing
|
|
715
715
|
|