ascertain 2.1.0 → 3.0.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.
package/README.md CHANGED
@@ -12,135 +12,237 @@ Zero-dependency, high-performance schema validator for Node.js and browsers.
12
12
 
13
13
  ## Features
14
14
 
15
- - **Zero dependencies**: Minimal footprint, no external dependencies
16
- - **High performance**: Compiles schemas to optimized JavaScript functions
17
- - **Type-safe**: Full TypeScript support with type inference
18
- - **Flexible schemas**: Supports AND, OR, optional, and tuple operators
19
- - **Type casting**: Built-in parsers for numbers, dates, JSON, base64, and more
20
- - **Object validation**: Validate keys and values with `$keys`, `$values`, and `$strict`
21
- - **Detailed errors**: Clear error messages with paths for debugging
22
-
23
- ## Installation
15
+ - **Zero dependencies** - Minimal footprint, no external dependencies
16
+ - **High performance** - Compiles schemas to optimized JS functions (~6x faster than dynamic validation)
17
+ - **Type-safe** - Full TypeScript support with type inference
18
+ - **Flexible schemas** - AND, OR, optional, tuple, discriminated operators
19
+ - **Type casting** - Built-in parsers for numbers (hex, octal, binary), dates, JSON, base64
20
+ - **Object validation** - Validate keys/values with `$keys`, `$values`, `$strict`
21
+ - **Partial validation** - `createValidator` validates subsets with type narrowing
22
+ - **Detailed errors** - Clear error messages with paths for debugging
23
+ - **Standard Schema v1** - Interoperable with tRPC, TanStack Form, and other ecosystem tools
24
+
25
+ ## Install
24
26
 
25
27
  ```bash
26
28
  npm install ascertain
27
- # or
28
- pnpm add ascertain
29
29
  ```
30
30
 
31
31
  ## Quick Start
32
32
 
33
33
  ```typescript
34
- import { ascertain, compile, or, optional } from 'ascertain';
34
+ import { ascertain, or, optional } from 'ascertain';
35
35
 
36
- // Define a schema
37
- const userSchema = {
36
+ ascertain({
38
37
  name: String,
39
38
  age: Number,
39
+ role: or('admin', 'user'),
40
40
  email: optional(String),
41
- role: or('admin', 'user', 'guest'),
42
- };
43
-
44
- // Validate data (throws on invalid)
45
- ascertain(userSchema, userData, 'User');
41
+ }, userData);
46
42
  ```
47
43
 
48
44
  ## Performance
49
45
 
50
- Ascertain compiles schemas into optimized JavaScript functions at runtime. This compilation step generates specialized validation code that runs significantly faster than interpreting schemas on each validation.
51
-
52
- **For best performance, compile schemas once and reuse the validator:**
46
+ Ascertain compiles schemas into optimized JavaScript functions. Compiled validators run **~6x faster** than dynamic validation.
53
47
 
54
48
  ```typescript
55
49
  import { compile } from 'ascertain';
56
50
 
57
- // Compile once at startup
58
- const validateUser = compile(userSchema, 'User');
51
+ // Compile once
52
+ const validateUser = compile(userSchema);
59
53
 
60
- // Reuse for each validation (no recompilation)
54
+ // Validate many (no recompilation)
61
55
  validateUser(user1);
62
56
  validateUser(user2);
63
57
  ```
64
58
 
65
- Use `ascertain()` for one-off validations where convenience matters more than performance. Use `compile()` when validating the same schema repeatedly (e.g., API request handlers).
59
+ | When to use | Function | Speed |
60
+ |-------------|----------|-------|
61
+ | Repeated validation (API handlers, loops) | `compile()` | Fastest |
62
+ | One-off validation | `ascertain()` | Convenient |
66
63
 
67
64
  ### Benchmark
68
65
 
69
- | Library | Operations/sec | Relative Speed |
70
- |---------|----------------|----------------|
71
- | **Ascertain** | 58,962,264 | **1.0x (fastest)** |
72
- | AJV | 42,204,777 | 0.72x |
73
- | Zod | 32,309,133 | 0.55x |
66
+ | Library | Mode | Valid (ops/s) | Invalid (ops/s) |
67
+ |---------|------|---------------|-----------------|
68
+ | **Ascertain** | first-error | 80M | 41M |
69
+ | **Ascertain** | all-errors | 80M | 25M |
70
+ | AJV | first-error | 52M | 35M |
71
+ | AJV | all-errors | 52M | 20M |
72
+ | Zod | all-errors | 34M | 77K |
73
+
74
+ ## Schema Reference
75
+
76
+ | Schema | Validates | Example |
77
+ |--------|-----------|---------|
78
+ | `String`, `Number`, `Boolean` | Type check | `{ age: Number }` |
79
+ | `Date`, `Array`, `Object` | Instance check | `{ created: Date }` |
80
+ | `Function` | Any callable | `{ handler: Function }` |
81
+ | Primitives | Exact value | `{ status: 'active' }` |
82
+ | RegExp | Pattern match | `{ email: /^.+@.+$/ }` |
83
+ | `[Schema]` | Array of type | `{ tags: [String] }` |
84
+ | `{ key: Schema }` | Object shape | `{ user: { name: String } }` |
85
+ | `or(a, b, ...)` | Any match | `or(String, Number)` |
86
+ | `and(a, b, ...)` | All match | `and(Date, { toJSON: Function })` |
87
+ | `optional(s)` | Nullable | `optional(String)` |
88
+ | `tuple(a, b)` | Fixed array | `tuple(Number, Number)` |
89
+ | `discriminated(schemas, key)` | Tagged union | `discriminated([{ type: 'a' }, { type: 'b' }], 'type')` |
90
+
91
+ ### Special Symbols
74
92
 
75
- ```bash
76
- pnpm bench
93
+ ```typescript
94
+ import { $keys, $values, $strict } from 'ascertain';
95
+
96
+ const schema = {
97
+ [$keys]: /^[a-z]+$/, // Validate all keys
98
+ [$values]: Number, // Validate all values
99
+ [$strict]: true, // No extra properties
100
+ };
77
101
  ```
78
102
 
79
- ## Schema Types
103
+ ## Type Casting
80
104
 
81
- | Schema | Description | Example |
82
- |--------|-------------|---------|
83
- | Primitives | Match exact values | `42`, `'active'`, `true`, `null` |
84
- | Constructors | Validate by type | `String`, `Number`, `Boolean`, `Date` |
85
- | Arrays | Validate array items | `[Number]` (array of numbers) |
86
- | Objects | Validate properties | `{ name: String, age: Number }` |
87
- | RegExp | Match string patterns | `/^[a-z]+$/` |
88
- | `or()` | Match any schema | `or(String, Number)` |
89
- | `and()` | Match all schemas | `and(Date, { toJSON: Function })` |
90
- | `optional()` | Allow null/undefined | `optional(String)` |
91
- | `tuple()` | Fixed-length arrays | `tuple(Number, Number)` |
105
+ Parse strings into typed values (environment variables, query params):
92
106
 
93
- ### Object Validation Symbols
107
+ ```typescript
108
+ import { as } from 'ascertain';
109
+
110
+ as.number('42') // 42
111
+ as.number('3.14') // 3.14
112
+ as.number('0xFF') // 255 (hex)
113
+ as.number('0o77') // 63 (octal)
114
+ as.number('0b1010') // 10 (binary)
115
+ as.number('1e10') // 10000000000
116
+
117
+ as.boolean('true') // true
118
+ as.boolean('1') // true
119
+
120
+ as.time('500ms') // 500
121
+ as.time('30s') // 30000
122
+ as.time('5m') // 300000
123
+ as.time('2h') // 7200000
124
+ as.time('1d') // 86400000
125
+
126
+ as.date('2024-12-31') // Date object
127
+ as.array('a,b,c', ',') // ['a', 'b', 'c']
128
+ as.json('{"x":1}') // { x: 1 }
129
+ as.base64('dGVzdA==') // 'test'
130
+ ```
131
+
132
+ Invalid values return `TypeError` for deferred validation:
94
133
 
95
134
  ```typescript
96
- import { $keys, $values, $strict } from 'ascertain';
135
+ const config = {
136
+ port: as.number(process.env.PORT), // TypeError if invalid
137
+ host: as.string(process.env.HOST),
138
+ };
139
+
140
+ // Errors surface with clear paths
141
+ ascertain({ port: Number, host: String }, config);
142
+ // → TypeError: "Invalid value undefined, expected non-nullable"
143
+ ```
97
144
 
145
+ ## Patterns
146
+
147
+ ### Batch Validation
148
+
149
+ Compile once, validate many:
150
+
151
+ ```typescript
152
+ const validateUser = compile(userSchema);
153
+
154
+ const results = users.map((user, i) => {
155
+ if (validateUser(user)) {
156
+ return { index: i, valid: true };
157
+ }
158
+ return { index: i, valid: false, error: validateUser.issues[0].message };
159
+ });
160
+ ```
161
+
162
+ ### Discriminated Unions
163
+
164
+ Use `discriminated()` for efficient tagged union validation. Instead of trying each variant like `or()`, it checks the discriminant field first and only validates the matching variant:
165
+
166
+ ```typescript
167
+ import { compile, discriminated } from 'ascertain';
168
+
169
+ const messageSchema = discriminated([
170
+ { type: 'email', address: String },
171
+ { type: 'sms', phone: String },
172
+ { type: 'push', token: String },
173
+ ], 'type');
174
+
175
+ const validate = compile(messageSchema);
176
+
177
+ validate({ type: 'email', address: 'user@example.com' }); // true
178
+ validate({ type: 'sms', phone: '123456' }); // true
179
+ validate({ type: 'push', token: 123 }); // false
180
+ validate({ type: 'unknown' }); // false
181
+ ```
182
+
183
+ Discriminant values must be string, number, or boolean literals.
184
+
185
+ ### Conditional Rules
186
+
187
+ Use `or()` and `and()` for complex conditions:
188
+
189
+ ```typescript
98
190
  const schema = {
99
- [$keys]: /^[a-z]+$/, // All keys must match pattern
100
- [$values]: Number, // All values must be numbers
101
- [$strict]: true, // No extra properties allowed
191
+ type: or('email', 'sms'),
192
+ // Conditional: email requires address, sms requires phone
193
+ contact: or(
194
+ and({ type: 'email' }, { address: String }),
195
+ and({ type: 'sms' }, { phone: String }),
196
+ ),
102
197
  };
103
198
  ```
104
199
 
105
- ## Type Casting with `as`
200
+ ### Schema Composition
106
201
 
107
- Parse and validate values from strings (useful for environment variables, query params, etc.):
202
+ Build schemas from reusable parts:
108
203
 
109
204
  ```typescript
110
- import { as } from 'ascertain';
205
+ const addressSchema = {
206
+ street: String,
207
+ city: String,
208
+ zip: /^\d{5}$/,
209
+ };
210
+
211
+ const personSchema = {
212
+ name: String,
213
+ address: addressSchema,
214
+ };
111
215
 
112
- as.string('hello') // 'hello'
113
- as.number('42') // 42
114
- as.number('3.14') // 3.14
115
- as.number('1e10') // 10000000000
116
- as.number('0xFF') // 255 (hex)
117
- as.number('0o77') // 63 (octal)
118
- as.number('0b1010') // 10 (binary)
119
- as.boolean('true') // true
120
- as.boolean('1') // true
121
- as.date('2024-12-31') // Date object
122
- as.time('2m') // 120000 (milliseconds)
123
- as.time('1h') // 3600000
124
- as.array('a,b,c', ',') // ['a', 'b', 'c']
125
- as.json('{"x":1}') // { x: 1 }
126
- as.base64('dGVzdA==') // 'test'
216
+ const companySchema = {
217
+ name: String,
218
+ headquarters: addressSchema,
219
+ employees: [personSchema],
220
+ };
127
221
  ```
128
222
 
129
- Invalid values return a `TypeError` instead of throwing, enabling deferred validation:
223
+ ### Versioned Schemas
224
+
225
+ Version schemas as modules:
130
226
 
131
227
  ```typescript
132
- const config = {
133
- port: as.number(process.env.PORT), // TypeError if invalid
134
- host: as.string(process.env.HOST),
228
+ // schemas/user.v1.ts
229
+ export const userSchemaV1 = { name: String, email: String };
230
+
231
+ // schemas/user.v2.ts
232
+ export const userSchemaV2 = {
233
+ ...userSchemaV1,
234
+ phone: optional(String),
235
+ createdAt: Date,
135
236
  };
136
237
 
137
- // Errors surface during schema validation with clear paths
138
- ascertain({ port: Number, host: String }, config, 'Config');
238
+ // api/handler.ts
239
+ import { userSchemaV2 } from './schemas/user.v2';
240
+ const validate = compile(userSchemaV2);
139
241
  ```
140
242
 
141
- ## Config Validation with `createValidator`
243
+ ### Config Validation
142
244
 
143
- For configuration objects, `createValidator` provides type-safe partial validation:
245
+ Validate only what each module needs:
144
246
 
145
247
  ```typescript
146
248
  import { createValidator, as } from 'ascertain';
@@ -148,19 +250,71 @@ import { createValidator, as } from 'ascertain';
148
250
  const config = {
149
251
  app: { name: as.string(process.env.APP_NAME), port: as.number(process.env.PORT) },
150
252
  db: { host: as.string(process.env.DB_HOST), pool: as.number(process.env.DB_POOL) },
151
- redis: { url: as.string(process.env.REDIS_URL) },
253
+ cache: { ttl: as.time(process.env.CACHE_TTL) },
152
254
  };
153
255
 
154
- const validate = createValidator(config, 'Config');
256
+ const validate = createValidator(config);
155
257
 
156
258
  // Each module validates only what it needs
157
- const { app, db } = validate({
158
- app: { name: String, port: Number },
259
+ const { db } = validate({
159
260
  db: { host: String, pool: Number },
160
261
  });
161
- // app.name is typed as string
162
- // app.port is typed as number
163
- // redis is not accessible (TypeScript error)
262
+
263
+ db.host; // string - validated and typed
264
+ db.pool; // number - validated and typed
265
+ // db.xxx // TypeScript error - property doesn't exist
266
+
267
+ // cache not validated = not accessible
268
+ // cache.ttl // TypeScript error - cache not in returned type
269
+ ```
270
+
271
+ ## Compile Options
272
+
273
+ By default `compile()` stops at the first validation error (fastest for invalid data). Pass `{ allErrors: true }` to collect all errors:
274
+
275
+ ```typescript
276
+ import { compile } from 'ascertain';
277
+
278
+ const schema = { name: String, age: Number, active: Boolean };
279
+
280
+ // First-error mode (default) - stops at first failure
281
+ const validate = compile(schema);
282
+ if (!validate({ name: 123, age: 'bad', active: 'no' })) {
283
+ console.log(validate.issues.length); // 1
284
+ console.log(validate.issues[0].path); // ['name']
285
+ }
286
+
287
+ // All-errors mode - collects every failure
288
+ const validateAll = compile(schema, { allErrors: true });
289
+ if (!validateAll({ name: 123, age: 'bad', active: 'no' })) {
290
+ console.log(validateAll.issues.length); // 3
291
+ }
292
+ ```
293
+
294
+ ## Standard Schema
295
+
296
+ Wrap a schema for [Standard Schema v1](https://standardschema.dev/) compliance, enabling interoperability with tRPC, TanStack Form, and other ecosystem libraries:
297
+
298
+ ```typescript
299
+ import { standardSchema, or, optional } from 'ascertain';
300
+
301
+ const userValidator = standardSchema({
302
+ name: String,
303
+ age: Number,
304
+ role: or('admin', 'user'),
305
+ email: optional(String),
306
+ });
307
+
308
+ // Use as regular validator (throws on error)
309
+ userValidator({ name: 'Alice', age: 30, role: 'admin' });
310
+
311
+ // Use Standard Schema interface (returns result object)
312
+ const result = userValidator['~standard'].validate(unknownData);
313
+ if (result.issues) {
314
+ console.log(result.issues);
315
+ } else {
316
+ console.log(result.value);
317
+ }
164
318
  ```
165
319
 
166
320
  ## Complete Example
@@ -169,55 +323,39 @@ const { app, db } = validate({
169
323
  import { compile, or, optional, and, tuple, $keys, $values, $strict, as } from 'ascertain';
170
324
 
171
325
  const schema = {
172
- // Type validation
173
326
  id: Number,
174
327
  name: String,
175
- active: Boolean,
176
-
177
- // Pattern matching
178
328
  email: /^[^@]+@[^@]+$/,
329
+ status: or('active', 'inactive', 'pending'),
330
+ role: optional(or('admin', 'user')),
179
331
 
180
- // Union types
181
- status: or('pending', 'active', 'disabled'),
182
-
183
- // Optional fields
184
- nickname: optional(String),
185
-
186
- // Nested objects
187
332
  profile: {
188
- bio: String,
189
- links: [String],
333
+ bio: optional(String),
334
+ avatar: optional(String),
190
335
  },
191
336
 
192
- // Combined constraints
193
- createdAt: and(Date, { toISOString: Function }),
194
-
195
- // Tuples
196
- coordinates: tuple(Number, Number),
197
-
198
- // Dynamic objects
199
- metadata: {
337
+ settings: {
200
338
  [$keys]: /^[a-z_]+$/,
201
- [$values]: or(String, Number),
339
+ [$values]: or(String, Number, Boolean),
202
340
  [$strict]: true,
203
341
  },
204
342
 
205
- // Parsed values
206
- retryCount: as.number(process.env.RETRY_COUNT),
343
+ coordinates: optional(tuple(Number, Number)),
344
+ createdAt: and(Date, { toISOString: Function }),
345
+
346
+ retries: as.number(process.env.MAX_RETRIES),
207
347
  timeout: as.time(process.env.TIMEOUT),
208
348
  };
209
349
 
210
- const validate = compile(schema, 'AppConfig');
211
-
212
- // Throws TypeError with detailed path on validation failure
213
- validate(data);
350
+ const validate = compile(schema);
351
+ if (!validate(data)) {
352
+ console.error(validate.issues);
353
+ }
214
354
  ```
215
355
 
216
356
  ## License
217
357
 
218
- [The MIT License](http://opensource.org/licenses/MIT)
219
-
220
- Copyright (c) 2019-2026 Ivan Zakharchanka
358
+ [MIT](http://opensource.org/licenses/MIT) - Ivan Zakharchanka
221
359
 
222
360
  [npm-url]: https://www.npmjs.com/package/ascertain
223
361
  [downloads-image]: https://img.shields.io/npm/dw/ascertain.svg?maxAge=43200