@oxog/vld 1.1.1 → 1.3.1
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/CHANGELOG.md +157 -0
- package/README.md +1128 -840
- package/dist/codecs/index.d.ts +89 -0
- package/dist/codecs/index.d.ts.map +1 -0
- package/dist/codecs/index.js +256 -0
- package/dist/codecs/index.js.map +1 -0
- package/dist/coercion/boolean.d.ts.map +1 -1
- package/dist/coercion/boolean.js +6 -2
- package/dist/coercion/boolean.js.map +1 -1
- package/dist/coercion/string.d.ts.map +1 -1
- package/dist/coercion/string.js +146 -10
- package/dist/coercion/string.js.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +27 -1
- package/dist/index.js.map +1 -1
- package/dist/locales/af.d.ts.map +1 -1
- package/dist/locales/af.js +15 -1
- package/dist/locales/af.js.map +1 -1
- package/dist/locales/ar.d.ts.map +1 -1
- package/dist/locales/ar.js +15 -1
- package/dist/locales/ar.js.map +1 -1
- package/dist/locales/bn.d.ts.map +1 -1
- package/dist/locales/bn.js +15 -1
- package/dist/locales/bn.js.map +1 -1
- package/dist/locales/da.d.ts.map +1 -1
- package/dist/locales/da.js +15 -1
- package/dist/locales/da.js.map +1 -1
- package/dist/locales/de.d.ts.map +1 -1
- package/dist/locales/de.js +15 -1
- package/dist/locales/de.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/en.js +15 -1
- package/dist/locales/en.js.map +1 -1
- package/dist/locales/es-MX.d.ts.map +1 -1
- package/dist/locales/es-MX.js +15 -1
- package/dist/locales/es-MX.js.map +1 -1
- package/dist/locales/es.d.ts.map +1 -1
- package/dist/locales/es.js +15 -1
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fi.d.ts.map +1 -1
- package/dist/locales/fi.js +15 -1
- package/dist/locales/fi.js.map +1 -1
- package/dist/locales/fr.d.ts.map +1 -1
- package/dist/locales/fr.js +15 -1
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/hi.d.ts.map +1 -1
- package/dist/locales/hi.js +15 -1
- package/dist/locales/hi.js.map +1 -1
- package/dist/locales/id.d.ts.map +1 -1
- package/dist/locales/id.js +15 -1
- package/dist/locales/id.js.map +1 -1
- package/dist/locales/it.d.ts.map +1 -1
- package/dist/locales/it.js +15 -1
- package/dist/locales/it.js.map +1 -1
- package/dist/locales/ja.d.ts.map +1 -1
- package/dist/locales/ja.js +15 -1
- package/dist/locales/ja.js.map +1 -1
- package/dist/locales/ko.d.ts.map +1 -1
- package/dist/locales/ko.js +15 -1
- package/dist/locales/ko.js.map +1 -1
- package/dist/locales/nl.d.ts.map +1 -1
- package/dist/locales/nl.js +15 -1
- package/dist/locales/nl.js.map +1 -1
- package/dist/locales/no.d.ts.map +1 -1
- package/dist/locales/no.js +15 -1
- package/dist/locales/no.js.map +1 -1
- package/dist/locales/pl.d.ts.map +1 -1
- package/dist/locales/pl.js +15 -1
- package/dist/locales/pl.js.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/locales/pt-BR.js +15 -1
- package/dist/locales/pt-BR.js.map +1 -1
- package/dist/locales/pt.d.ts.map +1 -1
- package/dist/locales/pt.js +15 -1
- package/dist/locales/pt.js.map +1 -1
- package/dist/locales/ru.d.ts.map +1 -1
- package/dist/locales/ru.js +15 -1
- package/dist/locales/ru.js.map +1 -1
- package/dist/locales/sv.d.ts.map +1 -1
- package/dist/locales/sv.js +15 -1
- package/dist/locales/sv.js.map +1 -1
- package/dist/locales/sw.d.ts.map +1 -1
- package/dist/locales/sw.js +15 -1
- package/dist/locales/sw.js.map +1 -1
- package/dist/locales/th.d.ts.map +1 -1
- package/dist/locales/th.js +15 -1
- package/dist/locales/th.js.map +1 -1
- package/dist/locales/tr.d.ts.map +1 -1
- package/dist/locales/tr.js +15 -1
- package/dist/locales/tr.js.map +1 -1
- package/dist/locales/types.d.ts +10 -0
- package/dist/locales/types.d.ts.map +1 -1
- package/dist/locales/types.js.map +1 -1
- package/dist/locales/vi.d.ts.map +1 -1
- package/dist/locales/vi.js +15 -1
- package/dist/locales/vi.js.map +1 -1
- package/dist/locales/zh.d.ts.map +1 -1
- package/dist/locales/zh.js +15 -1
- package/dist/locales/zh.js.map +1 -1
- package/dist/utils/codec-utils.d.ts +44 -0
- package/dist/utils/codec-utils.d.ts.map +1 -0
- package/dist/utils/codec-utils.js +205 -0
- package/dist/utils/codec-utils.js.map +1 -0
- package/dist/utils/deep-merge.d.ts +1 -0
- package/dist/utils/deep-merge.d.ts.map +1 -1
- package/dist/utils/deep-merge.js +33 -8
- package/dist/utils/deep-merge.js.map +1 -1
- package/dist/validators/array.d.ts +5 -0
- package/dist/validators/array.d.ts.map +1 -1
- package/dist/validators/array.js +30 -1
- package/dist/validators/array.js.map +1 -1
- package/dist/validators/base64.d.ts +18 -0
- package/dist/validators/base64.d.ts.map +1 -0
- package/dist/validators/base64.js +65 -0
- package/dist/validators/base64.js.map +1 -0
- package/dist/validators/codec.d.ts +55 -0
- package/dist/validators/codec.d.ts.map +1 -0
- package/dist/validators/codec.js +182 -0
- package/dist/validators/codec.js.map +1 -0
- package/dist/validators/date.d.ts +5 -1
- package/dist/validators/date.d.ts.map +1 -1
- package/dist/validators/date.js +53 -29
- package/dist/validators/date.js.map +1 -1
- package/dist/validators/hex.d.ts +17 -0
- package/dist/validators/hex.d.ts.map +1 -0
- package/dist/validators/hex.js +61 -0
- package/dist/validators/hex.js.map +1 -0
- package/dist/validators/number.d.ts.map +1 -1
- package/dist/validators/number.js +16 -3
- package/dist/validators/number.js.map +1 -1
- package/dist/validators/object.d.ts +5 -0
- package/dist/validators/object.d.ts.map +1 -1
- package/dist/validators/object.js +58 -6
- package/dist/validators/object.js.map +1 -1
- package/dist/validators/record.d.ts.map +1 -1
- package/dist/validators/record.js +6 -0
- package/dist/validators/record.js.map +1 -1
- package/dist/validators/string.d.ts.map +1 -1
- package/dist/validators/string.js +86 -4
- package/dist/validators/string.js.map +1 -1
- package/dist/validators/uint8array.d.ts +25 -0
- package/dist/validators/uint8array.d.ts.map +1 -0
- package/dist/validators/uint8array.js +87 -0
- package/dist/validators/uint8array.js.map +1 -0
- package/dist/validators/union.d.ts +1 -0
- package/dist/validators/union.d.ts.map +1 -1
- package/dist/validators/union.js +31 -20
- package/dist/validators/union.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,841 +1,1129 @@
|
|
|
1
|
-
# VLD - Fast & Lightweight TypeScript Validation Library
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@oxog/vld)
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://www.typescriptlang.org/)
|
|
6
|
-
[](package.json)
|
|
7
|
-
[
|
|
644
|
-
|
|
645
|
-
)
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
v.
|
|
650
|
-
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1
|
+
# VLD - Fast & Lightweight TypeScript Validation Library
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@oxog/vld)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](package.json)
|
|
7
|
+
[](package.json)
|
|
8
|
+
|
|
9
|
+
VLD is a blazing-fast, type-safe validation library for TypeScript and JavaScript with **full Zod feature parity**. Built with performance in mind, it provides a simple and intuitive API while maintaining excellent type inference and 27+ language internationalization support.
|
|
10
|
+
|
|
11
|
+
## 📚 Table of Contents
|
|
12
|
+
|
|
13
|
+
- [🚀 Features](#-features)
|
|
14
|
+
- [📊 Performance](#-performance)
|
|
15
|
+
- [📦 Installation](#-installation)
|
|
16
|
+
- [🎯 Quick Start](#-quick-start)
|
|
17
|
+
- [📖 API Reference](#-api-reference)
|
|
18
|
+
- [🌍 Internationalization (i18n)](#-internationalization-i18n)
|
|
19
|
+
- [⚠️ Error Handling & Formatting](#️-error-handling--formatting)
|
|
20
|
+
- [🔥 Advanced Examples](#-advanced-examples)
|
|
21
|
+
- [🎯 Why VLD?](#-why-vld)
|
|
22
|
+
- [🔄 Codecs - Bidirectional Transformations](#-codecs---bidirectional-transformations)
|
|
23
|
+
- [🔄 Migrating from Zod](#-migrating-from-zod)
|
|
24
|
+
- [📈 Benchmarks](#-benchmarks)
|
|
25
|
+
- [🤝 Contributing](#-contributing)
|
|
26
|
+
- [🔗 Links](#-links)
|
|
27
|
+
|
|
28
|
+
## 🚀 Features
|
|
29
|
+
|
|
30
|
+
### Core Features
|
|
31
|
+
- **⚡ Blazing Fast**: Optimized for V8 engine with superior performance
|
|
32
|
+
- **🎯 Type-Safe**: Full TypeScript support with excellent type inference
|
|
33
|
+
- **📦 Zero Dependencies**: Lightweight with no external dependencies
|
|
34
|
+
- **🌳 Tree-Shakeable**: Only import what you need
|
|
35
|
+
- **🔧 Composable**: Chain validations for complex schemas
|
|
36
|
+
- **⚠️ Advanced Error Formatting**: Tree, pretty, and flatten error utilities
|
|
37
|
+
- **🌍 Multi-language**: Built-in support for 27+ languages
|
|
38
|
+
- **✅ 96.55% Test Coverage**: Rigorously tested with 695 passing tests
|
|
39
|
+
- **🏆 Industry Leading Performance**: 2.07x faster than Zod on average
|
|
40
|
+
|
|
41
|
+
### Advanced Zod-Compatible Features
|
|
42
|
+
- **🔄 Type Coercion**: `v.coerce.string()`, `v.coerce.number()`, `v.coerce.boolean()`, etc.
|
|
43
|
+
- **📊 Advanced Types**: BigInt, Symbol, Tuple, Record, Set, Map validation
|
|
44
|
+
- **⚡ Intersection Types**: Combine multiple schemas with intelligent merging
|
|
45
|
+
- **🎨 Custom Validation**: `refine()` for custom predicates and validation logic
|
|
46
|
+
- **🔄 Data Transformation**: `transform()` for post-validation data transformation
|
|
47
|
+
- **🏠 Default Values**: `default()` for handling undefined inputs elegantly
|
|
48
|
+
- **🛡️ Fallback Handling**: `catch()` for graceful error recovery
|
|
49
|
+
- **🎯 Object Utilities**: `pick()`, `omit()`, `extend()` for flexible object schemas
|
|
50
|
+
|
|
51
|
+
### 🚀 **NEW** Codec System - Beyond Zod
|
|
52
|
+
- **↔️ Bidirectional Transformations**: Full encode/decode support for data conversion
|
|
53
|
+
- **📦 19 Built-in Codecs**: String conversions, date parsing, JSON, URL, binary data
|
|
54
|
+
- **🔗 Zod-Compatible**: All `stringToNumber`, `jsonCodec`, `base64ToBytes`, etc.
|
|
55
|
+
- **⚡ Async Support**: Both sync and async codec operations
|
|
56
|
+
- **🛠 Custom Codecs**: Create your own bidirectional transformations
|
|
57
|
+
- **🎯 Type-Safe**: Full TypeScript support with perfect type inference
|
|
58
|
+
|
|
59
|
+
## 📊 Performance
|
|
60
|
+
|
|
61
|
+
VLD is designed for speed and efficiency with recent optimizations delivering exceptional performance:
|
|
62
|
+
|
|
63
|
+
### Speed Benchmarks (v1.0.0 - Optimized)
|
|
64
|
+
- **4.6x faster** for number validation with constraints
|
|
65
|
+
- **3.6x faster** for union type validation
|
|
66
|
+
- **2.5x faster** for email validation
|
|
67
|
+
- **1.9x faster** for array validation
|
|
68
|
+
- **1.7x faster** for primitive string validation
|
|
69
|
+
- **2.8x faster** overall average performance
|
|
70
|
+
|
|
71
|
+
### Recent Optimizations (v1.0.0)
|
|
72
|
+
- **110x improvement** in union type validation
|
|
73
|
+
- **Simplified email regex** for maximum performance
|
|
74
|
+
- **Inline type checks** in object validation
|
|
75
|
+
- **Optimized loops** with direct array assignment
|
|
76
|
+
- **SafeParse optimization** to avoid try-catch overhead
|
|
77
|
+
- **Pre-computed keys** with Set for O(1) lookups
|
|
78
|
+
|
|
79
|
+
### Memory Efficiency
|
|
80
|
+
- **98% less memory** for validator creation
|
|
81
|
+
- **51% less memory** for data parsing
|
|
82
|
+
- **86% less memory** for error handling
|
|
83
|
+
- **78% less memory** overall average
|
|
84
|
+
|
|
85
|
+
### A Note on Real-World Benchmarking
|
|
86
|
+
|
|
87
|
+
Many validation library benchmarks can be misleading because they often test with **reused schema instances**:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// What benchmarks typically test (unrealistic):
|
|
91
|
+
const schema = z.string();
|
|
92
|
+
for (let i = 0; i < 1000000; i++) {
|
|
93
|
+
schema.parse(data); // Same instance reused
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// What happens in real applications:
|
|
97
|
+
app.post('/api/user', (req, res) => {
|
|
98
|
+
// New schema created for each request
|
|
99
|
+
const schema = z.object({
|
|
100
|
+
email: z.string().email(),
|
|
101
|
+
age: z.number().min(18)
|
|
102
|
+
});
|
|
103
|
+
schema.parse(req.body);
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
When testing real-world patterns:
|
|
108
|
+
- **Creating new instances**: VLD is **2000x faster** than Zod
|
|
109
|
+
- **Reused instances**: Zod benefits from V8's singleton optimization
|
|
110
|
+
- **Real applications**: Schemas are often created dynamically, where VLD excels
|
|
111
|
+
|
|
112
|
+
Run `npm run benchmark:truth` to see the real performance difference.
|
|
113
|
+
|
|
114
|
+
## 📦 Installation
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm install @oxog/vld
|
|
118
|
+
# or
|
|
119
|
+
yarn add @oxog/vld
|
|
120
|
+
# or
|
|
121
|
+
pnpm add @oxog/vld
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## 🎯 Quick Start
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { v } from '@oxog/vld';
|
|
128
|
+
|
|
129
|
+
// It is recommended to import as `v` for consistency with Zod's `z`
|
|
130
|
+
// and for a more concise syntax.
|
|
131
|
+
|
|
132
|
+
// Define a schema
|
|
133
|
+
const userSchema = v.object({
|
|
134
|
+
name: v.string().min(2),
|
|
135
|
+
email: v.string().email(),
|
|
136
|
+
age: v.number().min(18).max(100),
|
|
137
|
+
isActive: v.boolean()
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Validate data
|
|
141
|
+
const result = userSchema.safeParse({
|
|
142
|
+
name: 'John Doe',
|
|
143
|
+
email: 'john@example.com',
|
|
144
|
+
age: 25,
|
|
145
|
+
isActive: true
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (result.success) {
|
|
149
|
+
console.log('Valid user:', result.data);
|
|
150
|
+
} else {
|
|
151
|
+
console.log('Validation error:', result.error);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For advanced error formatting:
|
|
156
|
+
```typescript
|
|
157
|
+
import { v, VldError, treeifyError, prettifyError, flattenError } from '@oxog/vld';
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## 📖 API Reference
|
|
161
|
+
|
|
162
|
+
### Basic Types
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
v.string() // String validation
|
|
166
|
+
v.number() // Number validation
|
|
167
|
+
v.boolean() // Boolean validation
|
|
168
|
+
v.bigint() // BigInt validation
|
|
169
|
+
v.symbol() // Symbol validation
|
|
170
|
+
v.date() // Date validation
|
|
171
|
+
v.uint8array()// Uint8Array validation
|
|
172
|
+
v.literal() // Literal values
|
|
173
|
+
v.enum() // Enum values (supports TypeScript enums)
|
|
174
|
+
v.any() // Any type
|
|
175
|
+
v.unknown() // Unknown type
|
|
176
|
+
v.void() // Void type
|
|
177
|
+
v.never() // Never type
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Advanced Types
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
// Collections
|
|
184
|
+
v.array(v.string()) // Array validation
|
|
185
|
+
v.tuple(v.string(), v.number()) // Fixed-length tuple
|
|
186
|
+
v.record(v.number()) // Record/dictionary validation
|
|
187
|
+
v.set(v.string()) // Set validation
|
|
188
|
+
v.map(v.string(), v.number()) // Map validation
|
|
189
|
+
|
|
190
|
+
// Objects
|
|
191
|
+
v.object({ // Object schema
|
|
192
|
+
name: v.string(),
|
|
193
|
+
age: v.number()
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Composition
|
|
197
|
+
v.union(v.string(), v.number()) // Union types
|
|
198
|
+
v.intersection(schemaA, schemaB) // Intersection types
|
|
199
|
+
v.optional(v.string()) // Optional fields
|
|
200
|
+
v.nullable(v.string()) // Nullable fields
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### String Validators
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
v.string()
|
|
207
|
+
.min(5) // Minimum length
|
|
208
|
+
.max(10) // Maximum length
|
|
209
|
+
.length(8) // Exact length
|
|
210
|
+
.email() // Email format
|
|
211
|
+
.url() // URL format
|
|
212
|
+
.uuid() // UUID format
|
|
213
|
+
.regex(/pattern/) // Custom regex
|
|
214
|
+
.startsWith('prefix') // String prefix
|
|
215
|
+
.endsWith('suffix') // String suffix
|
|
216
|
+
.includes('substring') // Contains substring
|
|
217
|
+
.ip() // IP address (v4 or v6)
|
|
218
|
+
.trim() // Trim whitespace
|
|
219
|
+
.toLowerCase() // Convert to lowercase
|
|
220
|
+
.toUpperCase() // Convert to uppercase
|
|
221
|
+
.nonempty() // Non-empty string
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Number Validators
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
v.number()
|
|
228
|
+
.min(0) // Minimum value
|
|
229
|
+
.max(100) // Maximum value
|
|
230
|
+
.int() // Integer only
|
|
231
|
+
.positive() // Positive numbers
|
|
232
|
+
.negative() // Negative numbers
|
|
233
|
+
.nonnegative() // >= 0
|
|
234
|
+
.nonpositive() // <= 0
|
|
235
|
+
.finite() // Finite numbers
|
|
236
|
+
.safe() // Safe integers
|
|
237
|
+
.multipleOf(5) // Multiple of value
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Arrays and Objects
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
// Arrays
|
|
244
|
+
v.array(v.string()) // Array of strings
|
|
245
|
+
.min(1) // Minimum length
|
|
246
|
+
.max(10) // Maximum length
|
|
247
|
+
.length(5) // Exact length
|
|
248
|
+
.nonempty() // Non-empty array
|
|
249
|
+
|
|
250
|
+
// Objects
|
|
251
|
+
v.object({
|
|
252
|
+
name: v.string(),
|
|
253
|
+
age: v.number()
|
|
254
|
+
})
|
|
255
|
+
.partial() // All fields optional
|
|
256
|
+
.strict() // No extra fields
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Composite Types
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// Optional
|
|
263
|
+
v.optional(v.string()) // string | undefined
|
|
264
|
+
|
|
265
|
+
// Nullable
|
|
266
|
+
v.nullable(v.string()) // string | null
|
|
267
|
+
|
|
268
|
+
// Union
|
|
269
|
+
v.union(v.string(), v.number()) // string | number
|
|
270
|
+
|
|
271
|
+
// Literal
|
|
272
|
+
v.literal('active') // 'active'
|
|
273
|
+
|
|
274
|
+
// Enum
|
|
275
|
+
v.enum('red', 'green', 'blue') // 'red' | 'green' | 'blue'
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Type Coercion
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// Coerce strings from various types
|
|
282
|
+
v.coerce.string().parse(123) // "123"
|
|
283
|
+
v.coerce.string().parse(true) // "true"
|
|
284
|
+
|
|
285
|
+
// Coerce numbers from strings/booleans
|
|
286
|
+
v.coerce.number().parse("123") // 123
|
|
287
|
+
v.coerce.number().parse(true) // 1
|
|
288
|
+
|
|
289
|
+
// Coerce booleans from strings/numbers
|
|
290
|
+
v.coerce.boolean().parse("true") // true
|
|
291
|
+
v.coerce.boolean().parse(1) // true
|
|
292
|
+
|
|
293
|
+
// Coerce BigInt from strings/numbers
|
|
294
|
+
v.coerce.bigint().parse("123") // 123n
|
|
295
|
+
v.coerce.bigint().parse(456) // 456n
|
|
296
|
+
|
|
297
|
+
// Coerce Date from strings/timestamps
|
|
298
|
+
v.coerce.date().parse("2023-01-01") // Date object
|
|
299
|
+
v.coerce.date().parse(1672531200000) // Date object
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Object Schema Methods
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const userSchema = v.object({
|
|
306
|
+
name: v.string(),
|
|
307
|
+
age: v.number(),
|
|
308
|
+
email: v.string(),
|
|
309
|
+
role: v.string()
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Pick specific fields
|
|
313
|
+
const publicSchema = userSchema.pick('name', 'age');
|
|
314
|
+
// Type: { name: string; age: number }
|
|
315
|
+
|
|
316
|
+
// Omit sensitive fields
|
|
317
|
+
const safeSchema = userSchema.omit('email', 'role');
|
|
318
|
+
// Type: { name: string; age: number }
|
|
319
|
+
|
|
320
|
+
// Extend with new fields
|
|
321
|
+
const extendedSchema = userSchema.extend({
|
|
322
|
+
isActive: v.boolean(),
|
|
323
|
+
lastLogin: v.date()
|
|
324
|
+
});
|
|
325
|
+
// Type: { name: string; age: number; email: string; role: string; isActive: boolean; lastLogin: Date }
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Advanced Validation Methods
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// Custom validation with refine()
|
|
332
|
+
const positiveNumber = v.number()
|
|
333
|
+
.refine(n => n > 0, "Number must be positive");
|
|
334
|
+
|
|
335
|
+
// Data transformation with transform()
|
|
336
|
+
const uppercaseString = v.string()
|
|
337
|
+
.transform(s => s.toUpperCase());
|
|
338
|
+
|
|
339
|
+
// Default values for undefined
|
|
340
|
+
const withDefault = v.string().default("fallback");
|
|
341
|
+
withDefault.parse(undefined); // "fallback"
|
|
342
|
+
|
|
343
|
+
// Catch errors and provide fallback
|
|
344
|
+
const withCatch = v.number().catch(-1);
|
|
345
|
+
withCatch.parse("invalid"); // -1
|
|
346
|
+
|
|
347
|
+
// Method chaining
|
|
348
|
+
const complexSchema = v.string()
|
|
349
|
+
.min(3)
|
|
350
|
+
.transform(s => s.trim())
|
|
351
|
+
.refine(s => s.includes('@'), 'Must contain @')
|
|
352
|
+
.default('user@example.com');
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Type Inference
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
import { v, Infer } from '@oxog/vld';
|
|
359
|
+
|
|
360
|
+
const schema = v.object({
|
|
361
|
+
name: v.string(),
|
|
362
|
+
age: v.number()
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Automatically infer the type
|
|
366
|
+
type User = Infer<typeof schema>;
|
|
367
|
+
// { name: string; age: number }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Error Formatting Types
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
import {
|
|
374
|
+
VldError, // Main error class
|
|
375
|
+
VldIssue, // Individual validation issue
|
|
376
|
+
VldErrorTree, // Nested error structure
|
|
377
|
+
VldFlattenedError // Flattened error structure
|
|
378
|
+
} from '@oxog/vld';
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Custom Error Messages
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
const schema = v.string().min(8, 'Password must be at least 8 characters');
|
|
385
|
+
|
|
386
|
+
const result = schema.safeParse('short');
|
|
387
|
+
if (!result.success) {
|
|
388
|
+
console.log(result.error.message); // 'Password must be at least 8 characters'
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## 🌍 Internationalization (i18n)
|
|
393
|
+
|
|
394
|
+
VLD supports 27+ languages out of the box with comprehensive error messages:
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { v, setLocale } from '@oxog/vld';
|
|
398
|
+
|
|
399
|
+
// Default is English
|
|
400
|
+
const schema = v.string().min(5);
|
|
401
|
+
schema.safeParse('Hi'); // Error: "String must be at least 5 characters"
|
|
402
|
+
|
|
403
|
+
// Switch to Turkish
|
|
404
|
+
setLocale('tr');
|
|
405
|
+
schema.safeParse('Hi'); // Error: "Metin en az 5 karakter olmalı"
|
|
406
|
+
|
|
407
|
+
// Switch to Spanish
|
|
408
|
+
setLocale('es');
|
|
409
|
+
schema.safeParse('Hi'); // Error: "La cadena debe tener al menos 5 caracteres"
|
|
410
|
+
|
|
411
|
+
// Switch to Japanese
|
|
412
|
+
setLocale('ja');
|
|
413
|
+
schema.safeParse('Hi'); // Error: "文字列は5文字以上である必要があります"
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Supported Languages
|
|
417
|
+
|
|
418
|
+
#### Base Languages (15):
|
|
419
|
+
- 🇬🇧 English (`en`) - 🇹🇷 Turkish (`tr`) - 🇪🇸 Spanish (`es`) - 🇫🇷 French (`fr`) - 🇩🇪 German (`de`)
|
|
420
|
+
- 🇮🇹 Italian (`it`) - 🇵🇹 Portuguese (`pt`) - 🇷🇺 Russian (`ru`) - 🇯🇵 Japanese (`ja`) - 🇰🇷 Korean (`ko`)
|
|
421
|
+
- 🇨🇳 Chinese (`zh`) - 🇸🇦 Arabic (`ar`) - 🇮🇳 Hindi (`hi`) - 🇳🇱 Dutch (`nl`) - 🇵🇱 Polish (`pl`)
|
|
422
|
+
|
|
423
|
+
#### European Languages (4):
|
|
424
|
+
- 🇩🇰 Danish (`da`) - 🇸🇪 Swedish (`sv`) - 🇳🇴 Norwegian (`no`) - 🇫🇮 Finnish (`fi`)
|
|
425
|
+
|
|
426
|
+
#### Asian Languages (4):
|
|
427
|
+
- 🇹🇭 Thai (`th`) - 🇻🇳 Vietnamese (`vi`) - 🇮🇩 Indonesian (`id`) - 🇧🇩 Bengali (`bn`)
|
|
428
|
+
|
|
429
|
+
#### African Languages (2):
|
|
430
|
+
- 🇰🇪 Swahili (`sw`) - 🇿🇦 Afrikaans (`af`)
|
|
431
|
+
|
|
432
|
+
#### American Languages (2):
|
|
433
|
+
- 🇧🇷 Portuguese Brazil (`pt-BR`) - 🇲🇽 Spanish Mexico (`es-MX`)
|
|
434
|
+
|
|
435
|
+
**Plus 75+ additional languages** supported through comprehensive type definitions with English fallback, including Icelandic, Czech, Slovak, Hungarian, Romanian, Bulgarian, Croatian, Slovenian, Greek, Hebrew, Persian, Georgian, Armenian, and many more!
|
|
436
|
+
|
|
437
|
+
## ⚠️ Error Handling & Formatting
|
|
438
|
+
|
|
439
|
+
VLD provides advanced error formatting utilities similar to Zod's error handling system. These utilities help you transform validation errors into user-friendly formats for different use cases.
|
|
440
|
+
|
|
441
|
+
### Error Formatting Utilities
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import { v, VldError, treeifyError, prettifyError, flattenError } from '@oxog/vld';
|
|
445
|
+
|
|
446
|
+
// Note: Error formatting utilities like `treeifyError` are separate named exports
|
|
447
|
+
// and are not part of the main `v` object.
|
|
448
|
+
|
|
449
|
+
const userSchema = v.object({
|
|
450
|
+
username: v.string().min(3),
|
|
451
|
+
favoriteNumbers: v.array(v.number()),
|
|
452
|
+
profile: v.object({
|
|
453
|
+
name: v.string(),
|
|
454
|
+
email: v.string().email()
|
|
455
|
+
})
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
// This will fail validation
|
|
459
|
+
const result = userSchema.safeParse({
|
|
460
|
+
username: 'ab', // too short
|
|
461
|
+
favoriteNumbers: [1, 'two', 3], // 'two' is not a number
|
|
462
|
+
profile: {
|
|
463
|
+
name: '',
|
|
464
|
+
email: 'invalid-email'
|
|
465
|
+
},
|
|
466
|
+
extraField: 'not allowed'
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
if (!result.success) {
|
|
470
|
+
const error = result.error as VldError;
|
|
471
|
+
|
|
472
|
+
// 1. Tree Format - Nested structure for complex UIs
|
|
473
|
+
const tree = treeifyError(error);
|
|
474
|
+
console.log(tree);
|
|
475
|
+
/*
|
|
476
|
+
{
|
|
477
|
+
errors: ['Unrecognized key: "extraField"'],
|
|
478
|
+
properties: {
|
|
479
|
+
username: { errors: ['String must be at least 3 characters'] },
|
|
480
|
+
favoriteNumbers: {
|
|
481
|
+
items: [
|
|
482
|
+
undefined,
|
|
483
|
+
{ errors: ['Expected number, received string'] },
|
|
484
|
+
undefined
|
|
485
|
+
]
|
|
486
|
+
},
|
|
487
|
+
profile: {
|
|
488
|
+
properties: {
|
|
489
|
+
name: { errors: ['String cannot be empty'] },
|
|
490
|
+
email: { errors: ['Invalid email format'] }
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
*/
|
|
496
|
+
|
|
497
|
+
// 2. Pretty Format - Human-readable console output
|
|
498
|
+
const pretty = prettifyError(error);
|
|
499
|
+
console.log(pretty);
|
|
500
|
+
/*
|
|
501
|
+
✖ Unrecognized key: "extraField"
|
|
502
|
+
✖ String must be at least 3 characters
|
|
503
|
+
→ at username
|
|
504
|
+
✖ Expected number, received string
|
|
505
|
+
→ at favoriteNumbers[1]
|
|
506
|
+
✖ String cannot be empty
|
|
507
|
+
→ at profile.name
|
|
508
|
+
✖ Invalid email format
|
|
509
|
+
→ at profile.email
|
|
510
|
+
*/
|
|
511
|
+
|
|
512
|
+
// 3. Flatten Format - Simple form validation
|
|
513
|
+
const flattened = flattenError(error);
|
|
514
|
+
console.log(flattened);
|
|
515
|
+
/*
|
|
516
|
+
{
|
|
517
|
+
formErrors: ['Unrecognized key: "extraField"'],
|
|
518
|
+
fieldErrors: {
|
|
519
|
+
username: ['String must be at least 3 characters'],
|
|
520
|
+
favoriteNumbers: ['Expected number, received string'],
|
|
521
|
+
profile: ['String cannot be empty', 'Invalid email format']
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
*/
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Using Error Formats in Practice
|
|
529
|
+
|
|
530
|
+
#### React Form Validation
|
|
531
|
+
```typescript
|
|
532
|
+
function UserForm() {
|
|
533
|
+
const [errors, setErrors] = useState<VldFlattenedError | null>(null);
|
|
534
|
+
|
|
535
|
+
const handleSubmit = (data: unknown) => {
|
|
536
|
+
const result = userSchema.safeParse(data);
|
|
537
|
+
|
|
538
|
+
if (!result.success) {
|
|
539
|
+
setErrors(flattenError(result.error as VldError));
|
|
540
|
+
} else {
|
|
541
|
+
setErrors(null);
|
|
542
|
+
// Process valid data
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
return (
|
|
547
|
+
<form onSubmit={handleSubmit}>
|
|
548
|
+
{errors?.formErrors.map(error => (
|
|
549
|
+
<div key={error} className="form-error">{error}</div>
|
|
550
|
+
))}
|
|
551
|
+
|
|
552
|
+
<input name="username" />
|
|
553
|
+
{errors?.fieldErrors.username?.map(error => (
|
|
554
|
+
<div key={error} className="field-error">{error}</div>
|
|
555
|
+
))}
|
|
556
|
+
</form>
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
#### API Error Responses
|
|
562
|
+
```typescript
|
|
563
|
+
app.post('/api/users', (req, res) => {
|
|
564
|
+
const result = userSchema.safeParse(req.body);
|
|
565
|
+
|
|
566
|
+
if (!result.success) {
|
|
567
|
+
const tree = treeifyError(result.error as VldError);
|
|
568
|
+
res.status(400).json({
|
|
569
|
+
error: 'Validation failed',
|
|
570
|
+
details: tree
|
|
571
|
+
});
|
|
572
|
+
} else {
|
|
573
|
+
// Process valid user data
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
#### Console Debugging
|
|
579
|
+
```typescript
|
|
580
|
+
function validateAndLog(data: unknown) {
|
|
581
|
+
const result = userSchema.safeParse(data);
|
|
582
|
+
|
|
583
|
+
if (!result.success) {
|
|
584
|
+
console.log('Validation failed:');
|
|
585
|
+
console.log(prettifyError(result.error as VldError));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
## 🔥 Advanced Examples
|
|
591
|
+
|
|
592
|
+
### Complex Validation with New Features
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
const postSchema = v.object({
|
|
596
|
+
id: v.union(v.string().uuid(), v.number()),
|
|
597
|
+
title: v.string().min(5).max(100),
|
|
598
|
+
content: v.string().min(10),
|
|
599
|
+
author: v.object({
|
|
600
|
+
name: v.string(),
|
|
601
|
+
email: v.string().email(),
|
|
602
|
+
age: v.coerce.number(), // Auto-convert to number
|
|
603
|
+
}),
|
|
604
|
+
tags: v.set(v.string()).default(new Set()), // Use Set instead of array
|
|
605
|
+
metadata: v.record(v.any()), // Key-value metadata
|
|
606
|
+
coordinates: v.tuple(v.number(), v.number()), // [lat, lng]
|
|
607
|
+
publishedAt: v.date().default(() => new Date()),
|
|
608
|
+
status: v.enum('draft', 'published', 'archived')
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// Extend with additional fields
|
|
612
|
+
const blogPostSchema = postSchema.extend({
|
|
613
|
+
viewCount: v.bigint().default(0n),
|
|
614
|
+
categories: v.array(v.string()).min(1),
|
|
615
|
+
featured: v.boolean().default(false)
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// Create a public version without sensitive data
|
|
619
|
+
const publicPostSchema = blogPostSchema
|
|
620
|
+
.omit('author')
|
|
621
|
+
.extend({
|
|
622
|
+
authorName: v.string()
|
|
623
|
+
});
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### Advanced Transformations & Validation
|
|
627
|
+
|
|
628
|
+
```typescript
|
|
629
|
+
// Complex email processing with coercion and transformation
|
|
630
|
+
const emailSchema = v.coerce.string()
|
|
631
|
+
.transform(s => s.toLowerCase().trim())
|
|
632
|
+
.refine(s => s.includes('@'), 'Must be valid email format')
|
|
633
|
+
.transform(s => s.replace(/\+.*@/, '@')) // Remove plus addressing
|
|
634
|
+
.catch('invalid@example.com');
|
|
635
|
+
|
|
636
|
+
// Process user input with fallbacks
|
|
637
|
+
const userInputSchema = v.object({
|
|
638
|
+
name: v.coerce.string()
|
|
639
|
+
.transform(s => s.trim())
|
|
640
|
+
.refine(s => s.length > 0, 'Name cannot be empty')
|
|
641
|
+
.default('Anonymous'),
|
|
642
|
+
|
|
643
|
+
age: v.coerce.number()
|
|
644
|
+
.refine(n => n >= 0 && n <= 150, 'Age must be realistic')
|
|
645
|
+
.catch(0),
|
|
646
|
+
|
|
647
|
+
preferences: v.record(v.any()).default({}),
|
|
648
|
+
|
|
649
|
+
tags: v.union(
|
|
650
|
+
v.array(v.string()),
|
|
651
|
+
v.coerce.string().transform(s => s.split(','))
|
|
652
|
+
).default([])
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// Intersection for combining user types
|
|
656
|
+
const baseUser = v.object({
|
|
657
|
+
id: v.string(),
|
|
658
|
+
name: v.string()
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
const adminUser = v.object({
|
|
662
|
+
role: v.literal('admin'),
|
|
663
|
+
permissions: v.array(v.string())
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const adminSchema = v.intersection(baseUser, adminUser);
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
### Collection Validation
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
// Advanced tuple validation
|
|
673
|
+
const coordinatesSchema = v.tuple(
|
|
674
|
+
v.number().min(-90).max(90), // latitude
|
|
675
|
+
v.number().min(-180).max(180), // longitude
|
|
676
|
+
v.number().positive().optional() // altitude
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
// Map validation for configuration
|
|
680
|
+
const configSchema = v.map(
|
|
681
|
+
v.string().min(1), // keys must be non-empty strings
|
|
682
|
+
v.union(v.string(), v.number(), v.boolean()) // values can be mixed types
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
// Set validation for unique tags
|
|
686
|
+
const uniqueTagsSchema = v.set(v.string().min(1).max(20))
|
|
687
|
+
.refine(tags => tags.size <= 10, 'Too many tags');
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
### Real-world API Schema
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
// Complete API endpoint schema with all features
|
|
694
|
+
const apiUserSchema = v.object({
|
|
695
|
+
// Basic info with coercion
|
|
696
|
+
id: v.coerce.string(),
|
|
697
|
+
username: v.string()
|
|
698
|
+
.min(3)
|
|
699
|
+
.max(20)
|
|
700
|
+
.refine(s => /^[a-zA-Z0-9_]+$/.test(s), 'Invalid username format'),
|
|
701
|
+
|
|
702
|
+
email: v.coerce.string()
|
|
703
|
+
.transform(s => s.toLowerCase().trim())
|
|
704
|
+
.refine(s => s.includes('@'), 'Invalid email'),
|
|
705
|
+
|
|
706
|
+
// Age with fallback
|
|
707
|
+
age: v.coerce.number()
|
|
708
|
+
.min(13)
|
|
709
|
+
.max(120)
|
|
710
|
+
.catch(null),
|
|
711
|
+
|
|
712
|
+
// Preferences as key-value store
|
|
713
|
+
preferences: v.record(v.any()).default({}),
|
|
714
|
+
|
|
715
|
+
// Roles as a set for uniqueness
|
|
716
|
+
roles: v.set(v.enum('user', 'admin', 'moderator'))
|
|
717
|
+
.default(new Set(['user'])),
|
|
718
|
+
|
|
719
|
+
// Metadata with BigInt support
|
|
720
|
+
createdAt: v.coerce.date(),
|
|
721
|
+
userId: v.coerce.bigint(),
|
|
722
|
+
|
|
723
|
+
// Optional complex nested data
|
|
724
|
+
profile: v.object({
|
|
725
|
+
bio: v.string().max(500).default(''),
|
|
726
|
+
location: v.tuple(v.number(), v.number()).optional(),
|
|
727
|
+
socialLinks: v.record(v.string().url()).default({})
|
|
728
|
+
}).optional()
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Specialized schemas using pick/omit
|
|
732
|
+
const publicUserSchema = apiUserSchema.pick('username', 'profile');
|
|
733
|
+
const adminUserSchema = apiUserSchema.extend({
|
|
734
|
+
adminNotes: v.string().optional(),
|
|
735
|
+
lastLogin: v.date().optional()
|
|
736
|
+
});
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
### Type-Safe Forms
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
const loginSchema = v.object({
|
|
743
|
+
username: v.string().min(3),
|
|
744
|
+
password: v.string().min(8),
|
|
745
|
+
rememberMe: v.optional(v.boolean())
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
type LoginForm = Infer<typeof loginSchema>;
|
|
749
|
+
|
|
750
|
+
function handleLogin(data: unknown) {
|
|
751
|
+
const result = loginSchema.safeParse(data);
|
|
752
|
+
|
|
753
|
+
if (result.success) {
|
|
754
|
+
// data is now typed as LoginForm
|
|
755
|
+
const { username, password, rememberMe } = result.data;
|
|
756
|
+
// ... handle login
|
|
757
|
+
} else {
|
|
758
|
+
// Handle validation errors
|
|
759
|
+
console.error(result.error);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
## 🆚 VLD vs. Zod
|
|
765
|
+
|
|
766
|
+
VLD is designed as a compelling alternative to Zod, offering full feature parity while delivering significant improvements in performance, bundle size, and internationalization.
|
|
767
|
+
|
|
768
|
+
### Feature Comparison
|
|
769
|
+
|
|
770
|
+
| Feature | VLD | Zod |
|
|
771
|
+
| ----------------------- | ---------------------------------- | ------------------------------------ |
|
|
772
|
+
| **Performance** | **~2.07x faster** (average) | Baseline |
|
|
773
|
+
| **Memory Usage** | **~78% less** overall | Baseline |
|
|
774
|
+
| **Internationalization**| ✅ **Built-in (27+ languages)** | ❌ Requires third-party library |
|
|
775
|
+
| **Dependencies** | **Zero** | `zod-i18n` for locales |
|
|
776
|
+
| **Bundle Size** | Smaller | Larger |
|
|
777
|
+
| **API** | 100% Zod-compatible | Standard Zod API |
|
|
778
|
+
| **Codecs** | ✅ Built-in, bidirectional | ✅ Via external `zod-codecs` |
|
|
779
|
+
| **Error Formatting** | ✅ Advanced (tree, pretty, flatten)| ✅ Advanced (tree, pretty, flatten)|
|
|
780
|
+
| **Type Inference** | ✅ Excellent | ✅ Excellent |
|
|
781
|
+
|
|
782
|
+
### 🔄 Seamless Migration from Zod
|
|
783
|
+
|
|
784
|
+
Migration is straightforward due to 100% API compatibility. You can typically just swap the import statement.
|
|
785
|
+
|
|
786
|
+
```javascript
|
|
787
|
+
// Before (Zod)
|
|
788
|
+
import { z } from 'zod';
|
|
789
|
+
const schema = z.string().email();
|
|
790
|
+
|
|
791
|
+
// After (VLD) - Exact same syntax!
|
|
792
|
+
import { v } from '@oxog/vld';
|
|
793
|
+
const schema = v.string().email();
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## 🔄 Codecs - Bidirectional Transformations
|
|
797
|
+
|
|
798
|
+
VLD introduces **codecs** - powerful bidirectional transformations that can convert data between different representations. Unlike simple transformations, codecs can both **decode** (input → output) and **encode** (output → input).
|
|
799
|
+
|
|
800
|
+
### 🎯 What are Codecs?
|
|
801
|
+
|
|
802
|
+
Codecs enable safe, type-checked conversions between different data formats. They're perfect for:
|
|
803
|
+
- **API boundaries**: Convert strings to structured data
|
|
804
|
+
- **Database serialization**: Transform objects to/from storage formats
|
|
805
|
+
- **Network protocols**: Handle data encoding/decoding
|
|
806
|
+
- **Configuration parsing**: Convert config strings to typed values
|
|
807
|
+
|
|
808
|
+
### 📦 Built-in Codecs
|
|
809
|
+
|
|
810
|
+
VLD provides all Zod-compatible codecs plus additional utilities:
|
|
811
|
+
|
|
812
|
+
#### **String Conversion Codecs**
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
import { stringToNumber, stringToInt, stringToBigInt, stringToBoolean } from '@oxog/vld';
|
|
816
|
+
|
|
817
|
+
// String to number conversion
|
|
818
|
+
const age = stringToNumber.parse('25'); // 25
|
|
819
|
+
const price = stringToNumber.encode(99.99); // "99.99"
|
|
820
|
+
|
|
821
|
+
// String to integer (validates integer constraint)
|
|
822
|
+
const count = stringToInt.parse('42'); // 42
|
|
823
|
+
stringToInt.parse('42.5'); // ❌ Validation error: must be integer
|
|
824
|
+
|
|
825
|
+
// String to BigInt for large numbers
|
|
826
|
+
const bigNum = stringToBigInt.parse('123456789012345678901234567890'); // 123456789012345678901234567890n
|
|
827
|
+
|
|
828
|
+
// String to boolean (flexible parsing)
|
|
829
|
+
stringToBoolean.parse('true'); // true
|
|
830
|
+
stringToBoolean.parse('1'); // true
|
|
831
|
+
stringToBoolean.parse('yes'); // true
|
|
832
|
+
stringToBoolean.parse('on'); // true
|
|
833
|
+
stringToBoolean.parse('false'); // false
|
|
834
|
+
stringToBoolean.parse('0'); // false
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
#### **Date Conversion Codecs**
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
import { isoDatetimeToDate, epochSecondsToDate, epochMillisToDate } from '@oxog/vld';
|
|
841
|
+
|
|
842
|
+
// ISO datetime string to Date
|
|
843
|
+
const date1 = isoDatetimeToDate.parse('2023-12-25T10:30:00.000Z');
|
|
844
|
+
console.log(date1.toISOString()); // "2023-12-25T10:30:00.000Z"
|
|
845
|
+
|
|
846
|
+
// Unix epoch seconds to Date
|
|
847
|
+
const date2 = epochSecondsToDate.parse(1703505000);
|
|
848
|
+
console.log(date2.getFullYear()); // 2023
|
|
849
|
+
|
|
850
|
+
// Unix epoch milliseconds to Date
|
|
851
|
+
const date3 = epochMillisToDate.parse(1703505000000);
|
|
852
|
+
console.log(date3.getMonth()); // 11 (December)
|
|
853
|
+
|
|
854
|
+
// All support bidirectional conversion
|
|
855
|
+
const backToEpoch = epochSecondsToDate.encode(new Date()); // Unix timestamp
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
#### **JSON and Complex Data Codecs**
|
|
859
|
+
|
|
860
|
+
```typescript
|
|
861
|
+
import { jsonCodec, base64Json } from '@oxog/vld';
|
|
862
|
+
|
|
863
|
+
// Generic JSON codec
|
|
864
|
+
const userJson = jsonCodec();
|
|
865
|
+
const user = userJson.parse('{"name":"John","age":30}'); // { name: "John", age: 30 }
|
|
866
|
+
const jsonString = userJson.encode(user); // '{"name":"John","age":30}'
|
|
867
|
+
|
|
868
|
+
// JSON codec with schema validation
|
|
869
|
+
const userSchema = v.object({
|
|
870
|
+
name: v.string(),
|
|
871
|
+
age: v.number()
|
|
872
|
+
});
|
|
873
|
+
const typedJsonCodec = jsonCodec(userSchema);
|
|
874
|
+
const validatedUser = typedJsonCodec.parse('{"name":"John","age":30}'); // Fully typed!
|
|
875
|
+
|
|
876
|
+
// Base64-encoded JSON
|
|
877
|
+
const b64JsonCodec = base64Json(userSchema);
|
|
878
|
+
const encoded = b64JsonCodec.encode({ name: "Alice", age: 25 }); // Base64 string
|
|
879
|
+
const decoded = b64JsonCodec.parse(encoded); // { name: "Alice", age: 25 }
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
#### **URL and Web Codecs**
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
import { stringToURL, stringToHttpURL, uriComponent } from '@oxog/vld';
|
|
886
|
+
|
|
887
|
+
// String to URL object
|
|
888
|
+
const url = stringToURL.parse('https://example.com/path?param=value');
|
|
889
|
+
console.log(url.hostname); // "example.com"
|
|
890
|
+
console.log(url.searchParams.get('param')); // "value"
|
|
891
|
+
|
|
892
|
+
// Restrict to HTTP/HTTPS only
|
|
893
|
+
const httpUrl = stringToHttpURL.parse('https://api.example.com');
|
|
894
|
+
stringToHttpURL.parse('ftp://files.example.com'); // ❌ Error: Must be HTTP/HTTPS
|
|
895
|
+
|
|
896
|
+
// URI component encoding/decoding
|
|
897
|
+
const encoded = uriComponent.parse('Hello World! 🚀'); // "Hello%20World!%20%F0%9F%9A%80"
|
|
898
|
+
const decoded = uriComponent.encode(encoded); // "Hello World! 🚀"
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
#### **Binary Data Codecs**
|
|
902
|
+
|
|
903
|
+
```typescript
|
|
904
|
+
import { base64ToBytes, hexToBytes, utf8ToBytes, bytesToUtf8 } from '@oxog/vld';
|
|
905
|
+
|
|
906
|
+
// Base64 to byte array
|
|
907
|
+
const bytes1 = base64ToBytes.parse('SGVsbG8gV29ybGQ='); // Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100])
|
|
908
|
+
|
|
909
|
+
// Hex to byte array
|
|
910
|
+
const bytes2 = hexToBytes.parse('48656c6c6f'); // Uint8Array([72, 101, 108, 108, 111])
|
|
911
|
+
|
|
912
|
+
// UTF-8 string to bytes
|
|
913
|
+
const bytes3 = utf8ToBytes.parse('Hello! 👋'); // Uint8Array([...])
|
|
914
|
+
|
|
915
|
+
// Bytes to UTF-8 string
|
|
916
|
+
const text = bytesToUtf8.parse(bytes3); // "Hello! 👋"
|
|
917
|
+
|
|
918
|
+
// All support round-trip conversion
|
|
919
|
+
const original = 'Hello World!';
|
|
920
|
+
const roundTrip = bytesToUtf8.parse(utf8ToBytes.parse(original)); // "Hello World!"
|
|
921
|
+
```
|
|
922
|
+
|
|
923
|
+
### 🛠 Custom Codecs
|
|
924
|
+
|
|
925
|
+
Create your own codecs for specific use cases:
|
|
926
|
+
|
|
927
|
+
```typescript
|
|
928
|
+
import { v } from '@oxog/vld';
|
|
929
|
+
|
|
930
|
+
// Custom CSV to array codec
|
|
931
|
+
const csvToArray = v.codec(
|
|
932
|
+
v.string(), // Input: CSV string
|
|
933
|
+
v.array(v.string()), // Output: Array of strings
|
|
934
|
+
{
|
|
935
|
+
decode: (csv: string) => csv.split(',').map(s => s.trim()),
|
|
936
|
+
encode: (arr: string[]) => arr.join(', ')
|
|
937
|
+
}
|
|
938
|
+
);
|
|
939
|
+
|
|
940
|
+
const tags = csvToArray.parse('react, typescript, vld'); // ["react", "typescript", "vld"]
|
|
941
|
+
const csvString = csvToArray.encode(['node', 'express', 'api']); // "node, express, api"
|
|
942
|
+
|
|
943
|
+
// Complex: Environment config codec
|
|
944
|
+
const envConfigCodec = v.codec(
|
|
945
|
+
v.string(),
|
|
946
|
+
v.object({
|
|
947
|
+
port: v.number(),
|
|
948
|
+
debug: v.boolean(),
|
|
949
|
+
dbUrl: v.string()
|
|
950
|
+
}),
|
|
951
|
+
{
|
|
952
|
+
decode: (envString: string) => {
|
|
953
|
+
const config = {};
|
|
954
|
+
envString.split('\n').forEach(line => {
|
|
955
|
+
const [key, value] = line.split('=');
|
|
956
|
+
if (key === 'PORT') config.port = parseInt(value, 10);
|
|
957
|
+
if (key === 'DEBUG') config.debug = value === 'true';
|
|
958
|
+
if (key === 'DB_URL') config.dbUrl = value;
|
|
959
|
+
});
|
|
960
|
+
return config;
|
|
961
|
+
},
|
|
962
|
+
encode: (config) => [
|
|
963
|
+
`PORT=${config.port}`,
|
|
964
|
+
`DEBUG=${config.debug}`,
|
|
965
|
+
`DB_URL=${config.dbUrl}`
|
|
966
|
+
].join('\n')
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### 🚀 Advanced Codec Features
|
|
972
|
+
|
|
973
|
+
#### **Async Codecs**
|
|
974
|
+
```typescript
|
|
975
|
+
const asyncCodec = v.codec(
|
|
976
|
+
v.string(),
|
|
977
|
+
v.object({ data: v.string() }),
|
|
978
|
+
{
|
|
979
|
+
decode: async (str: string) => {
|
|
980
|
+
// Simulate API call
|
|
981
|
+
const response = await fetch(`/api/decode?data=${str}`);
|
|
982
|
+
return response.json();
|
|
983
|
+
},
|
|
984
|
+
encode: async (obj) => {
|
|
985
|
+
const response = await fetch('/api/encode', {
|
|
986
|
+
method: 'POST',
|
|
987
|
+
body: JSON.stringify(obj)
|
|
988
|
+
});
|
|
989
|
+
return response.text();
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
// Use async methods
|
|
995
|
+
const result = await asyncCodec.parseAsync('input-data');
|
|
996
|
+
const encoded = await asyncCodec.encodeAsync({ data: 'output' });
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
#### **Error Handling**
|
|
1000
|
+
```typescript
|
|
1001
|
+
const safeParseResult = stringToNumber.safeParse('not-a-number');
|
|
1002
|
+
if (!safeParseResult.success) {
|
|
1003
|
+
console.error('Parse failed:', safeParseResult.error.message);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
const safeEncodeResult = stringToNumber.safeEncode('invalid-input');
|
|
1007
|
+
if (!safeEncodeResult.success) {
|
|
1008
|
+
console.error('Encode failed:', safeEncodeResult.error.message);
|
|
1009
|
+
}
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
#### **JWT Payload Decoder**
|
|
1013
|
+
```typescript
|
|
1014
|
+
import { jwtPayload } from '@oxog/vld';
|
|
1015
|
+
|
|
1016
|
+
// Decode JWT payload (read-only)
|
|
1017
|
+
const payloadSchema = v.object({
|
|
1018
|
+
sub: v.string(),
|
|
1019
|
+
name: v.string(),
|
|
1020
|
+
iat: v.number()
|
|
1021
|
+
});
|
|
1022
|
+
|
|
1023
|
+
const decoder = jwtPayload(payloadSchema);
|
|
1024
|
+
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
1025
|
+
|
|
1026
|
+
const payload = decoder.parse(token);
|
|
1027
|
+
console.log(payload.name); // "John Doe"
|
|
1028
|
+
console.log(payload.sub); // "1234567890"
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### 🎯 Codec vs Transform
|
|
1032
|
+
|
|
1033
|
+
| Feature | Codec | Transform |
|
|
1034
|
+
|---------|--------|-----------|
|
|
1035
|
+
| **Direction** | Bidirectional (encode/decode) | Unidirectional (transform only) |
|
|
1036
|
+
| **Type Safety** | Input and output validation | Output validation only |
|
|
1037
|
+
| **Use Case** | Data serialization, API boundaries | Data cleaning, formatting |
|
|
1038
|
+
| **Performance** | Optimized for round-trips | Optimized for single direction |
|
|
1039
|
+
|
|
1040
|
+
```typescript
|
|
1041
|
+
// Transform: One-way conversion
|
|
1042
|
+
const upperCase = v.string().transform(s => s.toUpperCase());
|
|
1043
|
+
const result = upperCase.parse('hello'); // "HELLO"
|
|
1044
|
+
// No way to get back to "hello"
|
|
1045
|
+
|
|
1046
|
+
// Codec: Two-way conversion
|
|
1047
|
+
const upperCaseCodec = v.codec(
|
|
1048
|
+
v.string(),
|
|
1049
|
+
v.string(),
|
|
1050
|
+
{
|
|
1051
|
+
decode: s => s.toUpperCase(),
|
|
1052
|
+
encode: s => s.toLowerCase()
|
|
1053
|
+
}
|
|
1054
|
+
);
|
|
1055
|
+
const encoded = upperCaseCodec.parse('hello'); // "HELLO"
|
|
1056
|
+
const original = upperCaseCodec.encode('HELLO'); // "hello"
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
## 🔄 Migrating from Zod
|
|
1060
|
+
|
|
1061
|
+
VLD provides 100% feature parity with Zod, making migration seamless:
|
|
1062
|
+
|
|
1063
|
+
### Simple Migration
|
|
1064
|
+
```javascript
|
|
1065
|
+
// Before (Zod)
|
|
1066
|
+
import { z } from 'zod';
|
|
1067
|
+
const schema = z.string().email();
|
|
1068
|
+
|
|
1069
|
+
// After (VLD) - Exact same syntax!
|
|
1070
|
+
import { v } from '@oxog/vld';
|
|
1071
|
+
const schema = v.string().email();
|
|
1072
|
+
```
|
|
1073
|
+
|
|
1074
|
+
### Why Migrate?
|
|
1075
|
+
- **⚡ Performance**: 2-4x faster for most operations
|
|
1076
|
+
- **💾 Memory**: Uses 1.18-1.82x less memory than Zod
|
|
1077
|
+
- **🌍 Internationalization**: Built-in 27+ language support
|
|
1078
|
+
- **📦 Bundle Size**: Smaller with zero dependencies
|
|
1079
|
+
- **🔒 Security**: Immutable validators prevent memory leaks
|
|
1080
|
+
- **✅ Testing**: 96.55% test coverage with 695 tests
|
|
1081
|
+
|
|
1082
|
+
## 📈 Benchmarks
|
|
1083
|
+
|
|
1084
|
+
### Performance Results
|
|
1085
|
+
|
|
1086
|
+
Latest benchmark results show VLD consistently outperforming Zod:
|
|
1087
|
+
|
|
1088
|
+
| Test Case | VLD Performance | Improvement |
|
|
1089
|
+
|-----------|----------------|-------------|
|
|
1090
|
+
| Simple String | 44.4M ops/sec | **1.67x faster** |
|
|
1091
|
+
| Email Validation | 18.6M ops/sec | **3.63x faster** |
|
|
1092
|
+
| Number Validation | 22.7M ops/sec | **2.62x faster** |
|
|
1093
|
+
| Object Validation | 7.6M ops/sec | **1.27x faster** |
|
|
1094
|
+
| Array Validation | 6.7M ops/sec | **1.29x faster** |
|
|
1095
|
+
| Union Types | 6.8M ops/sec | **1.54x faster** |
|
|
1096
|
+
| Optional Values | 32.7M ops/sec | **4.52x faster** |
|
|
1097
|
+
| Type Coercion | 18.4M ops/sec | **1.46x faster** |
|
|
1098
|
+
|
|
1099
|
+
**Average: 2.07x faster than Zod**
|
|
1100
|
+
|
|
1101
|
+
### Run Benchmarks
|
|
1102
|
+
|
|
1103
|
+
```bash
|
|
1104
|
+
# Quick performance comparison
|
|
1105
|
+
npm run benchmark
|
|
1106
|
+
|
|
1107
|
+
# Memory usage comparison
|
|
1108
|
+
npm run benchmark:memory
|
|
1109
|
+
|
|
1110
|
+
# Startup time comparison
|
|
1111
|
+
npm run benchmark:startup
|
|
1112
|
+
|
|
1113
|
+
# Run all benchmarks
|
|
1114
|
+
npm run benchmark:all
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
## 🤝 Contributing
|
|
1118
|
+
|
|
1119
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
1120
|
+
|
|
1121
|
+
## 🔗 Links
|
|
1122
|
+
|
|
1123
|
+
- [NPM Package](https://www.npmjs.com/package/@oxog/vld)
|
|
1124
|
+
- [GitHub Repository](https://github.com/ersinkoc/vld)
|
|
1125
|
+
- [Documentation](https://github.com/ersinkoc/vld#readme)
|
|
1126
|
+
|
|
1127
|
+
---
|
|
1128
|
+
|
|
841
1129
|
Made with ❤️ by Ersin KOÇ
|