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/src/index.ts CHANGED
@@ -1,22 +1,3 @@
1
- /**
2
- * Abstract base class for schema operators.
3
- *
4
- * Provides a common constructor that enforces having at least one schema.
5
- *
6
- * @template T - The type of data the operator validates.
7
- * @abstract
8
- * @internal
9
- */
10
- abstract class Operator<T> {
11
- constructor(public readonly schemas: Schema<T>[]) {
12
- if (schemas.length === 0) {
13
- throw new TypeError(`Operation schema ${this.constructor.name} must have at least one element`);
14
- }
15
- }
16
- }
17
-
18
- // https://standardschema.dev/
19
-
20
1
  /**
21
2
  * Symbol for validating object keys against a schema.
22
3
  */
@@ -30,6 +11,66 @@ export const $values = Symbol.for('@@values');
30
11
  */
31
12
  export const $strict = Symbol.for('@@strict');
32
13
 
14
+ const $op = Symbol.for('@@op');
15
+
16
+ const OR = Symbol.for('@@or');
17
+ const AND = Symbol.for('@@and');
18
+ const OPTIONAL = Symbol.for('@@optional');
19
+ const TUPLE = Symbol.for('@@tuple');
20
+ const DISCRIMINATED = Symbol.for('@@discriminated');
21
+
22
+ type Mutable<T> = { -readonly [K in keyof T]: T[K] };
23
+
24
+ interface OrShape<T> {
25
+ readonly schemas: Schema<T>[];
26
+ readonly [$op]: typeof OR;
27
+ }
28
+ interface AndShape<T> {
29
+ readonly schemas: Schema<T>[];
30
+ readonly [$op]: typeof AND;
31
+ }
32
+ interface OptionalShape<T> {
33
+ readonly schemas: Schema<T>[];
34
+ readonly [$op]: typeof OPTIONAL;
35
+ }
36
+ interface TupleShape<T> {
37
+ readonly schemas: Schema<T>[];
38
+ readonly [$op]: typeof TUPLE;
39
+ }
40
+ interface DiscriminatedShape<T> {
41
+ readonly schemas: Schema<T>[];
42
+ readonly [$op]: typeof DISCRIMINATED;
43
+ readonly key: string;
44
+ }
45
+
46
+ type Tagged<T> = OrShape<T> | AndShape<T> | OptionalShape<T> | TupleShape<T> | DiscriminatedShape<T>;
47
+
48
+ const OrCtor = function <T>(this: OrShape<T>, schemas: Schema<T>[]) {
49
+ (this as Mutable<OrShape<T>>).schemas = schemas;
50
+ } as unknown as { new <T>(schemas: Schema<T>[]): OrShape<T>; prototype: { [$op]: typeof OR } };
51
+ OrCtor.prototype[$op] = OR;
52
+
53
+ const AndCtor = function <T>(this: AndShape<T>, schemas: Schema<T>[]) {
54
+ (this as Mutable<AndShape<T>>).schemas = schemas;
55
+ } as unknown as { new <T>(schemas: Schema<T>[]): AndShape<T>; prototype: { [$op]: typeof AND } };
56
+ AndCtor.prototype[$op] = AND;
57
+
58
+ const OptionalCtor = function <T>(this: OptionalShape<T>, schema: Schema<T>) {
59
+ (this as Mutable<OptionalShape<T>>).schemas = [schema];
60
+ } as unknown as { new <T>(schema: Schema<T>): OptionalShape<T>; prototype: { [$op]: typeof OPTIONAL } };
61
+ OptionalCtor.prototype[$op] = OPTIONAL;
62
+
63
+ const TupleCtor = function <T>(this: TupleShape<T>, schemas: Schema<T>[]) {
64
+ (this as Mutable<TupleShape<T>>).schemas = schemas;
65
+ } as unknown as { new <T>(schemas: Schema<T>[]): TupleShape<T>; prototype: { [$op]: typeof TUPLE } };
66
+ TupleCtor.prototype[$op] = TUPLE;
67
+
68
+ const DiscriminatedCtor = function <T>(this: DiscriminatedShape<T>, schemas: Schema<T>[], key: string) {
69
+ (this as Mutable<DiscriminatedShape<T>>).schemas = schemas;
70
+ (this as Mutable<DiscriminatedShape<T>>).key = key;
71
+ } as unknown as { new <T>(schemas: Schema<T>[], key: string): DiscriminatedShape<T>; prototype: { [$op]: typeof DISCRIMINATED } };
72
+ DiscriminatedCtor.prototype[$op] = DISCRIMINATED;
73
+
33
74
  /**
34
75
  * Represents a schema for validating data.
35
76
  *
@@ -44,167 +85,58 @@ export type Schema<T> =
44
85
  ? Schema<A>[] | unknown
45
86
  : unknown;
46
87
 
47
- class Or<T> extends Operator<T> {}
48
88
  /**
49
89
  * Operator for validating data against any of the provided schemas (logical OR).
50
- *
51
- * Creates a schema that accepts data matching any one of the provided schemas.
52
- * This is useful for creating union types or alternative validation paths.
53
- *
54
- * @template T - The type of data the operator validates.
55
- * @param schemas - Multiple schemas where at least one must match the data.
56
- * @returns A schema that validates data against any of the provided schemas.
57
- *
58
- * @example
59
- * ```typescript
60
- * import { or, ascertain } from 'ascertain';
61
- *
62
- * // Create a schema that accepts either a string or number
63
- * const stringOrNumber = or(String, Number);
64
- *
65
- * ascertain(stringOrNumber, "hello", "value"); // ✓ Valid
66
- * ascertain(stringOrNumber, 42, "value"); // ✓ Valid
67
- * ascertain(stringOrNumber, true, "value"); // ✗ Throws error
68
- *
69
- * // Union of literal values
70
- * const statusSchema = or('pending', 'completed', 'failed');
71
- * ascertain(statusSchema, 'pending', "status"); // ✓ Valid
72
- *
73
- * // Complex schema combinations
74
- * const userIdSchema = or(Number, { id: Number, temp: Boolean });
75
- * ascertain(userIdSchema, 123, "userId"); // ✓ Valid
76
- * ascertain(userIdSchema, { id: 456, temp: true }, "userId"); // ✓ Valid
77
- * ```
78
90
  */
79
- export const or = <T>(...schemas: Schema<T>[]) => new Or(schemas);
91
+ export const or = <T>(...schemas: Schema<T>[]): OrShape<T> => {
92
+ if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
93
+ return new OrCtor(schemas);
94
+ };
80
95
 
81
- class And<T> extends Operator<T> {}
82
96
  /**
83
97
  * Operator for validating data against all provided schemas (logical AND).
84
- *
85
- * Creates a schema that requires data to match every one of the provided schemas.
86
- * This is useful for combining multiple validation requirements or adding constraints.
87
- *
88
- * @template T - The type of data the operator validates.
89
- * @param schemas - Multiple schemas that all must match the data.
90
- * @returns A schema that validates data against all of the provided schemas.
91
- *
92
- * @example
93
- * ```typescript
94
- * import { and, ascertain } from 'ascertain';
95
- *
96
- * // Combine object schema with additional constraints
97
- * const userSchema = and(
98
- * { name: String, age: Number },
99
- * { age: Number } // Additional constraint
100
- * );
101
- *
102
- * ascertain(userSchema, { name: "John", age: 25 }, "user"); // ✓ Valid
103
- *
104
- * // Ensure an object is both a Date and has specific methods
105
- * const validDateSchema = and(Date, { toISOString: Function });
106
- * ascertain(validDateSchema, new Date(), "date"); // ✓ Valid
107
- *
108
- * ```
109
98
  */
110
- export const and = <T>(...schemas: Schema<T>[]) => new And(schemas);
99
+ export const and = <T>(...schemas: Schema<T>[]): AndShape<T> => {
100
+ if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
101
+ return new AndCtor(schemas);
102
+ };
111
103
 
112
- class Optional<T> extends Operator<T> {
113
- constructor(schema: Schema<T>) {
114
- super([schema]);
115
- }
116
- }
117
104
  /**
118
105
  * Operator for making a schema optional (nullable).
119
- *
120
- * Creates a schema that accepts the provided schema or null/undefined values.
121
- * This is useful for optional object properties or nullable fields.
122
- *
123
- * @template T - The type of data the operator validates.
124
- * @param schema - The schema to make optional.
125
- * @returns A schema that validates data against the provided schema or accepts null/undefined.
126
- *
127
- * @example
128
- * ```typescript
129
- * import { optional, ascertain } from 'ascertain';
130
- *
131
- * // Optional string field
132
- * const userSchema = {
133
- * name: String,
134
- * nickname: optional(String),
135
- * age: Number
136
- * };
137
- *
138
- * // All of these are valid
139
- * ascertain(userSchema, {
140
- * name: "John",
141
- * nickname: "Johnny",
142
- * age: 30
143
- * }, "user"); // ✓ Valid
144
- *
145
- * ascertain(userSchema, {
146
- * name: "Jane",
147
- * nickname: null,
148
- * age: 25
149
- * }, "user"); // ✓ Valid
150
- *
151
- * ascertain(userSchema, {
152
- * name: "Bob",
153
- * age: 35
154
- * // nickname is undefined
155
- * }, "user"); // ✓ Valid
156
- *
157
- * // Optional complex objects
158
- * const profileSchema = {
159
- * id: Number,
160
- * settings: optional({
161
- * theme: String,
162
- * notifications: Boolean
163
- * })
164
- * };
165
- * ```
166
106
  */
167
- export const optional = <T>(schema: Schema<T>) => new Optional(schema);
107
+ export const optional = <T>(schema: Schema<T>): OptionalShape<T> => new OptionalCtor(schema);
168
108
 
169
- class Tuple<T> extends Operator<T> {}
170
109
  /**
171
110
  * Operator for validating data against a fixed-length tuple of schemas.
111
+ */
112
+ export const tuple = <T>(...schemas: Schema<T>[]): TupleShape<T> => {
113
+ if (schemas.length === 0) throw new TypeError('Operator requires at least one schema');
114
+ return new TupleCtor(schemas);
115
+ };
116
+
117
+ /**
118
+ * Operator for validating data against a discriminated union.
172
119
  *
173
- * Creates a schema that validates arrays with a specific length and type for each position.
174
- * This is useful for coordinate pairs, RGB values, or any fixed-structure data.
120
+ * Optimizes validation by checking the discriminant field first and only
121
+ * validating the matching variant. More efficient than `or()` for unions
122
+ * where each variant has a common field with a unique literal value.
175
123
  *
176
- * @template T - The type of data the operator validates (a tuple of types).
177
- * @param schemas - Schemas for each position in the tuple, in order.
178
- * @returns A schema that validates data as a tuple with the specified structure.
124
+ * @param schemas - Array of object schemas, each with a discriminant field containing a literal value.
125
+ * @param key - The name of the discriminant field present in all variants.
179
126
  *
180
127
  * @example
181
128
  * ```typescript
182
- * import { tuple, ascertain } from 'ascertain';
183
- *
184
- * // 2D coordinate tuple
185
- * const pointSchema = tuple(Number, Number);
186
- * ascertain(pointSchema, [10, 20], "point"); // ✓ Valid
187
- * ascertain(pointSchema, [1.5, 2.7], "point"); // ✓ Valid
188
- * ascertain(pointSchema, [10], "point"); // ✗ Throws error (too short)
189
- * ascertain(pointSchema, [10, 20, 30], "point"); // ✗ Throws error (too long)
190
- *
191
- * // RGB color tuple
192
- * const colorSchema = tuple(Number, Number, Number);
193
- * ascertain(colorSchema, [255, 128, 0], "color"); // ✓ Valid
194
- *
195
- * // Mixed type tuple
196
- * const userInfoSchema = tuple(String, Number, Boolean);
197
- * ascertain(userInfoSchema, ["Alice", 25, true], "userInfo"); // ✓ Valid
198
- *
199
- * // Nested tuple
200
- * const lineSchema = tuple(
201
- * tuple(Number, Number), // start point
202
- * tuple(Number, Number) // end point
203
- * );
204
- * ascertain(lineSchema, [[0, 0], [10, 10]], "line"); // ✓ Valid
129
+ * const messageSchema = discriminated([
130
+ * { type: 'email', address: String },
131
+ * { type: 'sms', phone: String },
132
+ * { type: 'push', token: String },
133
+ * ], 'type');
205
134
  * ```
206
135
  */
207
- export const tuple = <T>(...schemas: Schema<T>[]) => new Tuple(schemas);
136
+ export const discriminated = <T>(schemas: Schema<T>[], key: string): DiscriminatedShape<T> => {
137
+ if (schemas.length === 0) throw new TypeError('discriminated requires at least one schema');
138
+ return new DiscriminatedCtor(schemas, key);
139
+ };
208
140
 
209
141
  /**
210
142
  * Decodes a base64-encoded string to UTF-8.
@@ -214,7 +146,9 @@ export const tuple = <T>(...schemas: Schema<T>[]) => new Tuple(schemas);
214
146
  * @param value - The base64-encoded string to decode.
215
147
  * @returns The decoded UTF-8 string.
216
148
  */
217
- export const fromBase64 = typeof Buffer === 'undefined' ? (value: string) => atob(value) : (value: string) => Buffer.from(value, 'base64').toString('utf-8');
149
+ export const fromBase64 =
150
+ /* c8 ignore next */
151
+ typeof Buffer === 'undefined' ? (value: string) => atob(value) : (value: string) => Buffer.from(value, 'base64').toString('utf-8');
218
152
 
219
153
  const MULTIPLIERS = {
220
154
  ms: 1,
@@ -277,7 +211,7 @@ export const as = {
277
211
  return value[0] === '-' ? -result : result;
278
212
  }
279
213
 
280
- const result = value.includes('.') || value.includes('e') || value.includes('E') ? parseFloat(value) : parseInt(value, 10);
214
+ const result = value.trim() ? Number(value) : NaN;
281
215
  return Number.isNaN(result) ? asError(`Invalid value ${value}, expected a valid number`) : result;
282
216
  },
283
217
  /**
@@ -389,232 +323,387 @@ class Context {
389
323
  }
390
324
  }
391
325
 
392
- const codeGenCollectErrors = (errorsAlias: string, code: string, extra: string = '') => `try {${code}} catch (e) {${errorsAlias}.push(e.message);${extra}}`;
393
- const codeGenExpectNoErrors = (errorsAlias: string) => `if (${errorsAlias}.length !== 0) { throw new TypeError(${errorsAlias}.join('\\n')); }`;
394
- const codeGenExpectNonError = (valueAlias: string, path: string) =>
395
- `if (${valueAlias} instanceof Error) { throw new TypeError(\`\${${valueAlias}.message} for path "${path}".\`); }`;
396
- const codeGenExpectNonNullable = (valueAlias: string, path: string) =>
397
- `if (${valueAlias} === null || ${valueAlias} === undefined) { throw new TypeError(\`Invalid value \${${valueAlias}} for path "${path}", expected non-nullable.\`); }`;
398
- const codeGenExpectObject = (valueAlias: string, path: string, instanceOf: string) =>
399
- `if (typeof ${valueAlias} !== 'object') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected an instance of ${instanceOf}\`); }`;
400
- const codeGenExpectArray = (valueAlias: string, path: string) =>
401
- `if (!Array.isArray(${valueAlias})) { throw new TypeError(\`Invalid instance of \${${valueAlias}.constructor?.name} for path "${path}", expected an instance of Array.\`); }`;
402
-
403
- const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, path: string): string => {
404
- if (schema instanceof And) {
405
- const valueAlias = context.unique('v');
406
- const errorsAlias = context.unique('err');
407
- const code = schema.schemas.map((s) => `try { ${codeGen(s, context, valueAlias, path)} } catch (e) { ${errorsAlias}.push(e.message); }`).join('\n');
408
- return `// And
409
- const ${errorsAlias} = [];
410
- const ${valueAlias} = ${valuePath};
411
- ${code}
412
- ${codeGenExpectNoErrors(errorsAlias)}
413
- `;
414
- } else if (schema instanceof Or) {
415
- const valueAlias = context.unique('v');
416
- const errorsAlias = context.unique('err');
417
- const code = schema.schemas
418
- .map((s) => codeGen(s, context, valueAlias, path))
419
- .reduceRight((result, code) => codeGenCollectErrors(errorsAlias, code, result), codeGenExpectNoErrors(errorsAlias));
420
- return `// Or
421
- const ${errorsAlias} = [];
422
- const ${valueAlias} = ${valuePath};
423
- ${code}
424
- `;
425
- } else if (schema instanceof Optional) {
426
- const valueAlias = context.unique('v');
427
- return `// Optional
428
- const ${valueAlias} = ${valuePath};
429
- if (${valueAlias} !== undefined && ${valueAlias} !== null) { ${codeGen(schema.schemas[0], context, valueAlias, path)} }
430
- `;
431
- } else if (schema instanceof Tuple) {
432
- const valueAlias = context.unique('v');
433
- const errorsAlias = context.unique('err');
434
- const code: string[] = [
435
- '// Tuple',
436
- `const ${valueAlias} = ${valuePath};`,
437
- `const ${errorsAlias} = [];`,
438
- codeGenExpectNonNullable(valueAlias, path),
439
- codeGenExpectObject(valueAlias, path, 'Array'),
440
- codeGenExpectArray(valueAlias, path),
441
- `if (${valueAlias}.length !== ${schema.schemas.length}) { throw new TypeError(\`Invalid tuple length \${${valueAlias}.length} for path "${path}", expected ${schema.schemas.length}.\`); }`,
442
- ...schema.schemas.map((s, idx) => codeGenCollectErrors(errorsAlias, codeGen(s, context, `${valueAlias}[${idx}]`, `${path}[${idx}]`))),
443
- codeGenExpectNoErrors(errorsAlias),
444
- ];
445
- return code.join('\n');
446
- } else if (typeof schema === 'function') {
447
- const valueAlias = context.unique('v');
448
- const code: string[] = [`const ${valueAlias} = ${valuePath};`, codeGenExpectNonNullable(valueAlias, path)];
449
- if ((schema as unknown) !== Error && !(schema?.prototype instanceof Error)) {
450
- code.push(codeGenExpectNonError(valueAlias, path));
326
+ type Mode =
327
+ | { fast: true; onFail?: string }
328
+ | { fast: false; firstError: true; issues: string; path: PropertyKey[]; pathExpr: string }
329
+ | { fast: false; firstError: false; issues: string; path: PropertyKey[]; pathExpr: string };
330
+
331
+ const isTagged = (schema: unknown): schema is Tagged<unknown> => (schema as Tagged<unknown>)?.[$op] !== undefined;
332
+
333
+ const childMode = (mode: Exclude<Mode, { fast: true }>, key: PropertyKey | { dynamic: string }): Mode => {
334
+ if (typeof key === 'object' && 'dynamic' in key) {
335
+ return {
336
+ fast: false,
337
+ firstError: mode.firstError,
338
+ issues: mode.issues,
339
+ path: mode.path,
340
+ pathExpr: `[${mode.path.map((k) => JSON.stringify(k)).join(',')}${mode.path.length ? ',' : ''}${key.dynamic}]`,
341
+ };
342
+ }
343
+ const newPath = [...mode.path, key];
344
+ return { fast: false, firstError: mode.firstError, issues: mode.issues, path: newPath, pathExpr: JSON.stringify(newPath) };
345
+ };
346
+
347
+ const toLiteral = (value: unknown): string => (typeof value === 'bigint' ? `${value}n` : JSON.stringify(value));
348
+
349
+ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode: Mode): string => {
350
+ const emit = mode.fast
351
+ ? null
352
+ : mode.firstError
353
+ ? (msg: string) => `${mode.issues} = [{ message: ${msg}, path: ${mode.pathExpr} }]; return ${mode.issues};`
354
+ : (msg: string) => `(${mode.issues} || (${mode.issues} = [])).push({ message: ${msg}, path: ${mode.pathExpr} });`;
355
+ const fail = mode.fast ? (mode.onFail ?? 'return false;') : '';
356
+
357
+ if (isTagged(schema)) {
358
+ const tag = schema[$op];
359
+ if (tag === AND) {
360
+ const valueAlias = context.unique('v');
361
+ const code = schema.schemas.map((s) => codeGen(s, context, valueAlias, mode)).join('\n');
362
+ return `const ${valueAlias} = ${valuePath};\n${code}`;
363
+ } else if (tag === OR) {
364
+ const valueAlias = context.unique('v');
365
+ const foundValid = context.unique('valid');
366
+ if (mode.fast) {
367
+ const branches = schema.schemas.map((s) => {
368
+ const branchValid = context.unique('valid');
369
+ const branchCode = codeGen(s, context, valueAlias, { ...mode, onFail: `${branchValid} = false;` });
370
+ return `if (!${foundValid}) { let ${branchValid} = true; ${branchCode} if (${branchValid}) { ${foundValid} = true; } }`;
371
+ });
372
+ return `const ${valueAlias} = ${valuePath};\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { ${fail} }`;
373
+ } else if (mode.firstError) {
374
+ const firstBranchIssues = context.unique('iss');
375
+ const branches = schema.schemas.map((s, idx) => {
376
+ const branchIssues = context.unique('iss');
377
+ const branchCode = codeGen(s, context, valueAlias, {
378
+ fast: false,
379
+ firstError: true,
380
+ issues: branchIssues,
381
+ path: mode.path,
382
+ pathExpr: mode.pathExpr,
383
+ }).replace(new RegExp(`; return ${branchIssues};`, 'g'), ';');
384
+ if (idx === 0) {
385
+ return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } else { ${firstBranchIssues} = ${branchIssues}; } }`;
386
+ }
387
+ return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } }`;
388
+ });
389
+ return `const ${valueAlias} = ${valuePath};\nlet ${firstBranchIssues};\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { return ${firstBranchIssues}; }`;
390
+ } else {
391
+ const localIssues = context.unique('iss');
392
+ const branches = schema.schemas.map((s) => {
393
+ const branchIssues = context.unique('iss');
394
+ const branchCode = codeGen(s, context, valueAlias, {
395
+ fast: false,
396
+ firstError: false,
397
+ issues: branchIssues,
398
+ path: mode.path,
399
+ pathExpr: mode.pathExpr,
400
+ });
401
+ return `if (!${foundValid}) { let ${branchIssues}; ${branchCode} if (!${branchIssues}) { ${foundValid} = true; } else { ${localIssues}.push(...${branchIssues}); } }`;
402
+ });
403
+ return `const ${valueAlias} = ${valuePath};\nconst ${localIssues} = [];\nlet ${foundValid} = false;\n${branches.join('\n')}\nif (!${foundValid}) { (${mode.issues} || (${mode.issues} = [])).push(...${localIssues}); }`;
404
+ }
405
+ } else if (tag === OPTIONAL) {
406
+ const valueAlias = context.unique('v');
407
+ return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== undefined && ${valueAlias} !== null) { ${codeGen((schema as OptionalShape<T>).schemas[0], context, valueAlias, mode)} }`;
408
+ } else if (tag === TUPLE) {
409
+ const valueAlias = context.unique('v');
410
+ if (mode.fast) {
411
+ return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || typeof ${valueAlias} !== 'object' || !Array.isArray(${valueAlias}) || ${valueAlias}.length !== ${schema.schemas.length}) { ${fail} }\n${schema.schemas.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, mode)).join('\n')}`;
412
+ } else {
413
+ return [
414
+ `const ${valueAlias} = ${valuePath};`,
415
+ `if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
416
+ `else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Array\``)} }`,
417
+ `else if (!Array.isArray(${valueAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}.constructor?.name}, expected an instance of Array\``)} }`,
418
+ `else if (${valueAlias}.length !== ${schema.schemas.length}) { ${emit!(`\`Invalid tuple length \${${valueAlias}.length}, expected ${schema.schemas.length}\``)} }`,
419
+ `else { ${schema.schemas.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`,
420
+ ].join('\n');
421
+ }
422
+ } else {
423
+ const { key, schemas } = schema as DiscriminatedShape<T>;
424
+ const valueAlias = context.unique('v');
425
+ const discriminantAlias = context.unique('d');
426
+ const keyStr = JSON.stringify(key);
427
+
428
+ const variants: { value: unknown; schema: Schema<T> }[] = [];
429
+ for (const s of schemas) {
430
+ if (typeof s !== 'object' || s === null || !(key in s)) {
431
+ throw new TypeError(`discriminated: each schema must have the discriminant key "${key}"`);
432
+ }
433
+ const discriminantValue = (s as Record<string, unknown>)[key];
434
+ if (typeof discriminantValue !== 'string' && typeof discriminantValue !== 'number' && typeof discriminantValue !== 'boolean') {
435
+ throw new TypeError(`discriminated: discriminant value must be a string, number, or boolean literal`);
436
+ }
437
+ variants.push({ value: discriminantValue, schema: s });
438
+ }
439
+
440
+ if (mode.fast) {
441
+ const branches = variants.map(({ value, schema: s }) => {
442
+ const branchCode = codeGen(s, context, valueAlias, mode);
443
+ return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${branchCode} }`;
444
+ });
445
+ return [
446
+ `const ${valueAlias} = ${valuePath};`,
447
+ `if (${valueAlias} === null || ${valueAlias} === undefined || typeof ${valueAlias} !== 'object' || ${valueAlias} instanceof Error) { ${fail} }`,
448
+ `const ${discriminantAlias} = ${valueAlias}[${keyStr}];`,
449
+ branches.join(' else ') + ` else { ${fail} }`,
450
+ ].join('\n');
451
+ } else {
452
+ const validValues = variants.map((v) => JSON.stringify(v.value)).join(', ');
453
+ const branches = variants.map(({ value, schema: s }) => {
454
+ const branchCode = codeGen(s, context, valueAlias, mode);
455
+ return `if (${discriminantAlias} === ${JSON.stringify(value)}) { ${branchCode} }`;
456
+ });
457
+ return [
458
+ `const ${valueAlias} = ${valuePath};`,
459
+ `if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
460
+ `else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an object\``)} }`,
461
+ `else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
462
+ `else {`,
463
+ ` const ${discriminantAlias} = ${valueAlias}[${keyStr}];`,
464
+ ` ${branches.join(' else ')} else { ${emit!(`\`Invalid discriminant value \${JSON.stringify(${discriminantAlias})}, expected one of: ${validValues}\``)} }`,
465
+ `}`,
466
+ ].join('\n');
467
+ }
451
468
  }
469
+ }
452
470
 
471
+ if (typeof schema === 'function') {
472
+ const valueAlias = context.unique('v');
453
473
  const name = (schema as { name?: string })?.name;
474
+ const s = schema as unknown;
454
475
  const primitiveType =
455
- name === 'String'
456
- ? 'string'
457
- : name === 'Number'
458
- ? 'number'
459
- : name === 'Boolean'
460
- ? 'boolean'
461
- : name === 'BigInt'
462
- ? 'bigint'
463
- : name === 'Symbol'
464
- ? 'symbol'
465
- : null;
466
-
467
- if (primitiveType) {
468
- code.push(
469
- `if (typeof ${valueAlias} !== '${primitiveType}') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected type ${schema?.name}\`); }`,
470
- );
471
- if (primitiveType === 'number') {
472
- code.push(
473
- `if (Number.isNaN(${valueAlias})) { throw new TypeError(\`Invalid value \${${valueAlias}} for path "${path}", expected a valid ${schema?.name}\`); }`,
474
- );
476
+ s === String ? 'string' : s === Number ? 'number' : s === Boolean ? 'boolean' : s === BigInt ? 'bigint' : s === Symbol ? 'symbol' : null;
477
+
478
+ if (mode.fast) {
479
+ if (primitiveType) {
480
+ const checks = [`typeof ${valueAlias} !== '${primitiveType}'`];
481
+ if (primitiveType === 'number') checks.push(`Number.isNaN(${valueAlias})`);
482
+ return `const ${valueAlias} = ${valuePath};\nif (${checks.join(' || ')}) { ${fail} }`;
483
+ } else if (name === 'Function') {
484
+ return `const ${valueAlias} = ${valuePath};\nif (typeof ${valueAlias} !== 'function') { ${fail} }`;
485
+ } else {
486
+ const isError = (schema as unknown) === Error || schema?.prototype instanceof Error;
487
+ const index = context.register(schema);
488
+ const registryAlias = context.unique('r');
489
+ return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = ctx.registry[${index}];\nif (${valueAlias} === null || ${valueAlias} === undefined${isError ? '' : ` || ${valueAlias} instanceof Error`} || (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) || (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) || Number.isNaN(${valueAlias}?.valueOf?.())) { ${fail} }`;
475
490
  }
476
- } else if (name === 'Function') {
477
- code.push(
478
- `if (typeof ${valueAlias} !== 'function') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected type Function\`); }`,
479
- );
480
491
  } else {
481
- const index = context.register(schema);
482
- const registryAlias = context.unique('r');
483
- code.push(
484
- `const ${registryAlias} = ctx.registry[${index}];`,
485
- `if (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) { throw new TypeError(\`Invalid instance of \${${valueAlias}?.constructor?.name} for path "${path}", expected an instance of ${schema?.name}\`); }`,
486
- `if (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) { throw new TypeError(\`Invalid type \${${valueAlias}?.constructor?.name} for path "${path}", expected type ${schema?.name}\`); }`,
487
- `if (Number.isNaN(${valueAlias}?.valueOf?.())) { throw new TypeError(\`Invalid value \${${valueAlias}} for path "${path}", expected a valid ${schema?.name}\`); }`,
488
- );
489
- }
490
- return code.join('\n');
491
- } else if (Array.isArray(schema)) {
492
- const valueAlias = context.unique('v');
493
- const code: string[] = [
494
- `const ${valueAlias} = ${valuePath};`,
495
- codeGenExpectNonNullable(valueAlias, path),
496
- codeGenExpectNonError(valueAlias, path),
497
- codeGenExpectObject(valueAlias, path, 'Array'),
498
- codeGenExpectArray(valueAlias, path),
499
- ];
500
- if (schema.length > 0) {
501
- const value = context.unique('val');
502
- const key = context.unique('key');
503
- const errorsAlias = context.unique('err');
504
- code.push(`const ${errorsAlias} = [];`);
505
-
506
- if (schema.length === 1) {
492
+ const code: string[] = [`const ${valueAlias} = ${valuePath};`];
493
+ if (primitiveType) {
494
+ code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
495
+ code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
496
+ code.push(`else if (typeof ${valueAlias} !== '${primitiveType}') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected type ${name}\``)} }`);
497
+ if (primitiveType === 'number')
498
+ code.push(`else if (Number.isNaN(${valueAlias})) { ${emit!(`\`Invalid value \${${valueAlias}}, expected a valid ${name}\``)} }`);
499
+ } else if (name === 'Function') {
500
+ code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
501
+ code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
502
+ code.push(`else if (typeof ${valueAlias} !== 'function') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected type Function\``)} }`);
503
+ } else {
504
+ const isError = (schema as unknown) === Error || schema?.prototype instanceof Error;
505
+ const index = context.register(schema);
506
+ const registryAlias = context.unique('r');
507
+ code.push(`const ${registryAlias} = ctx.registry[${index}];`);
508
+ code.push(`if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`);
509
+ if (!isError) code.push(`else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`);
507
510
  code.push(
508
- ...schema.map(
509
- (s) =>
510
- `for (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGenCollectErrors(errorsAlias, codeGen(s, context, value, `${path}[\${${key}}]`))} }`,
511
- ),
511
+ `else if (typeof ${valueAlias} === 'object' && !(${valueAlias} instanceof ${registryAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}?.constructor?.name}, expected an instance of ${name}\``)} }`,
512
512
  );
513
- } else {
514
513
  code.push(
515
- `if (${valueAlias}.length > ${schema.length}) { throw new TypeError(\`Invalid tuple length \${${valueAlias}.length} for path "${path}", expected ${schema.length}.\`); }`,
514
+ `else if (typeof ${valueAlias} !== 'object' && ${valueAlias}?.constructor !== ${registryAlias}) { ${emit!(`\`Invalid type \${${valueAlias}?.constructor?.name}, expected type ${name}\``)} }`,
516
515
  );
517
- code.push(...schema.map((s, idx) => codeGenCollectErrors(errorsAlias, codeGen(s, context, `${valueAlias}[${idx}]`, `${path}[${idx}]`))));
516
+ code.push(`else if (Number.isNaN(${valueAlias}?.valueOf?.())) { ${emit!(`\`Invalid value \${${valueAlias}}, expected a valid ${name}\``)} }`);
518
517
  }
519
-
520
- code.push(codeGenExpectNoErrors(errorsAlias));
518
+ return code.join('\n');
521
519
  }
522
- return code.join('\n');
523
- } else if (typeof schema === 'object' && schema !== null) {
524
- if (schema instanceof RegExp) {
525
- const valueAlias = context.unique('v');
526
- return `
527
- const ${valueAlias} = ${valuePath};
528
- ${codeGenExpectNonNullable(valueAlias, path)}
529
- ${codeGenExpectNonError(valueAlias, path)}
530
- if (!${schema.toString()}.test(String(${valueAlias}))) { throw new TypeError(\`Invalid value \${${valueAlias}} for path "${path}", expected to match ${schema.toString()}\`); }
531
- `;
520
+ }
521
+
522
+ if (Array.isArray(schema)) {
523
+ const valueAlias = context.unique('v');
524
+ if (mode.fast) {
525
+ let code = `const ${valueAlias} = ${valuePath};\nif (!Array.isArray(${valueAlias})) { ${fail} }`;
526
+ if (schema.length === 1) {
527
+ const value = context.unique('val');
528
+ const key = context.unique('key');
529
+ code += `\nfor (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGen(schema[0], context, value, mode)} }`;
530
+ } else if (schema.length > 1) {
531
+ code += `\nif (${valueAlias}.length > ${schema.length}) { ${fail} }`;
532
+ code += '\n' + schema.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, mode)).join('\n');
533
+ }
534
+ return code;
532
535
  } else {
533
- const valueAlias = context.unique('v');
534
536
  const code: string[] = [
535
537
  `const ${valueAlias} = ${valuePath};`,
536
- codeGenExpectNonNullable(valueAlias, path),
537
- codeGenExpectObject(valueAlias, path, 'Object'),
538
- codeGenExpectNonError(valueAlias, path),
538
+ `if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
539
+ `else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
540
+ `else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Array\``)} }`,
541
+ `else if (!Array.isArray(${valueAlias})) { ${emit!(`\`Invalid instance of \${${valueAlias}.constructor?.name}, expected an instance of Array\``)} }`,
539
542
  ];
540
- if ($keys in schema) {
541
- const keysAlias = context.unique('k');
542
- const errorsAlias = context.unique('err');
543
- const kAlias = context.unique('k');
544
- code.push(`
545
- const ${keysAlias} = Object.keys(${valueAlias});
546
- const ${errorsAlias} = [];
547
- for (const ${kAlias} of ${keysAlias}) { ${codeGenCollectErrors(errorsAlias, codeGen(schema[$keys], context, kAlias, `${path}[\${${kAlias}}]`))} }
548
- ${codeGenExpectNoErrors(errorsAlias)}
549
- `);
543
+ if (schema.length > 0) {
544
+ const value = context.unique('val');
545
+ const key = context.unique('key');
546
+ if (schema.length === 1) {
547
+ // Dynamic key - use runtime concat
548
+ code.push(
549
+ `else { for (let ${key} = 0; ${key} < ${valueAlias}.length; ${key}++) { const ${value} = ${valueAlias}[${key}]; ${codeGen(schema[0], context, value, childMode(mode, { dynamic: key }))} } }`,
550
+ );
551
+ } else {
552
+ code.push(
553
+ `else if (${valueAlias}.length > ${schema.length}) { ${emit!(`\`Invalid tuple length \${${valueAlias}.length}, expected ${schema.length}\``)} }`,
554
+ );
555
+ code.push(`else { ${schema.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`);
556
+ }
550
557
  }
551
- if ($values in schema) {
552
- const vAlias = context.unique('val');
553
- const kAlias = context.unique('k');
554
- const entriesAlias = context.unique('en');
555
- const errorsAlias = context.unique('err');
556
- code.push(`
557
- const ${entriesAlias} = Object.entries(${valueAlias});
558
- const ${errorsAlias} = [];
559
- for (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGenCollectErrors(errorsAlias, codeGen(schema[$values], context, vAlias, `${path}[\${${kAlias}}]`))} }
560
- ${codeGenExpectNoErrors(errorsAlias)}
561
- `);
558
+ return code.join('\n');
559
+ }
560
+ }
561
+
562
+ if (typeof schema === 'object' && schema !== null) {
563
+ if (schema instanceof RegExp) {
564
+ const valueAlias = context.unique('v');
565
+ if (mode.fast) {
566
+ return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined || ${valueAlias} instanceof Error || !${schema.toString()}.test(String(${valueAlias}))) { ${fail} }`;
567
+ } else {
568
+ return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }\nelse if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }\nelse if (!${schema.toString()}.test(String(${valueAlias}))) { ${emit!(`\`Invalid value \${${valueAlias}}, expected to match ${schema.toString()}\``)} }`;
562
569
  }
563
- if ($strict in schema && schema[$strict]) {
564
- const keysAlias = context.unique('k');
565
- const kAlias = context.unique('k');
566
- const extraAlias = context.unique('ex');
567
- code.push(`const ${keysAlias} = new Set(${JSON.stringify(Object.keys(schema))});`);
568
- code.push(`const ${extraAlias} = Object.keys(${valueAlias}).filter(${kAlias} => !${keysAlias}.has(${kAlias}));`);
569
- code.push(`if (${extraAlias}.length !== 0) { throw new TypeError(\`Extra properties: \${${extraAlias}}, are not allowed for path "${path}"\`); }`);
570
+ } else {
571
+ const valueAlias = context.unique('v');
572
+ if (mode.fast) {
573
+ let code = `const ${valueAlias} = ${valuePath};\nif (${valueAlias} === null || typeof ${valueAlias} !== 'object' || ${valueAlias} instanceof Error) { ${fail} }`;
574
+ if ($keys in schema) {
575
+ const keysAlias = context.unique('k');
576
+ const kAlias = context.unique('k');
577
+ code += `\nconst ${keysAlias} = Object.keys(${valueAlias});\nfor (const ${kAlias} of ${keysAlias}) { ${codeGen(schema[$keys], context, kAlias, mode)} }`;
578
+ }
579
+ if ($values in schema) {
580
+ const vAlias = context.unique('val');
581
+ const kAlias = context.unique('k');
582
+ const entriesAlias = context.unique('en');
583
+ code += `\nconst ${entriesAlias} = Object.entries(${valueAlias});\nfor (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGen(schema[$values], context, vAlias, mode)} }`;
584
+ }
585
+ if ($strict in schema && schema[$strict]) {
586
+ const keysAlias = context.unique('k');
587
+ const kAlias = context.unique('k');
588
+ const extraAlias = context.unique('ex');
589
+ code += `\nconst ${keysAlias} = new Set(${JSON.stringify(Object.keys(schema))});\nconst ${extraAlias} = Object.keys(${valueAlias}).filter(${kAlias} => !${keysAlias}.has(${kAlias}));\nif (${extraAlias}.length !== 0) { ${fail} }`;
590
+ }
591
+ code +=
592
+ '\n' +
593
+ Object.entries(schema)
594
+ .map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, mode))
595
+ .join('\n');
596
+ return code;
597
+ } else {
598
+ const code: string[] = [
599
+ `const ${valueAlias} = ${valuePath};`,
600
+ `if (${valueAlias} === null || ${valueAlias} === undefined) { ${emit!(`\`Invalid value \${${valueAlias}}, expected non-nullable\``)} }`,
601
+ `else if (typeof ${valueAlias} !== 'object') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected an instance of Object\``)} }`,
602
+ `else if (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }`,
603
+ 'else {',
604
+ ];
605
+ const innerCode: string[] = [];
606
+ if ($keys in schema) {
607
+ const keysAlias = context.unique('k');
608
+ const kAlias = context.unique('k');
609
+ innerCode.push(`const ${keysAlias} = Object.keys(${valueAlias});`);
610
+ // Dynamic key - use runtime concat
611
+ innerCode.push(`for (const ${kAlias} of ${keysAlias}) { ${codeGen(schema[$keys], context, kAlias, childMode(mode, { dynamic: kAlias }))} }`);
612
+ }
613
+ if ($values in schema) {
614
+ const vAlias = context.unique('val');
615
+ const kAlias = context.unique('k');
616
+ const entriesAlias = context.unique('en');
617
+ innerCode.push(`const ${entriesAlias} = Object.entries(${valueAlias});`);
618
+ // Dynamic key - use runtime concat
619
+ innerCode.push(
620
+ `for (const [${kAlias}, ${vAlias}] of ${entriesAlias}) { ${codeGen(schema[$values], context, vAlias, childMode(mode, { dynamic: kAlias }))} }`,
621
+ );
622
+ }
623
+ if ($strict in schema && schema[$strict]) {
624
+ const keysAlias = context.unique('k');
625
+ const kAlias = context.unique('k');
626
+ const extraAlias = context.unique('ex');
627
+ innerCode.push(`const ${keysAlias} = new Set(${JSON.stringify(Object.keys(schema))});`);
628
+ innerCode.push(`const ${extraAlias} = Object.keys(${valueAlias}).filter(${kAlias} => !${keysAlias}.has(${kAlias}));`);
629
+ innerCode.push(`if (${extraAlias}.length !== 0) { ${emit!(`\`Extra properties: \${${extraAlias}}, are not allowed\``)} }`);
630
+ }
631
+ // Static keys - pre-register paths
632
+ innerCode.push(...Object.entries(schema).map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, childMode(mode, key))));
633
+ code.push(innerCode.join('\n'), '}');
634
+ return code.join('\n');
570
635
  }
571
- code.push(...Object.entries(schema).map(([key, s]) => codeGen(s, context, `${valueAlias}[${JSON.stringify(key)}]`, `${path}.${key}`)));
572
- return `${code.join('\n')}`;
573
636
  }
574
- } else if (typeof schema === 'symbol') {
637
+ }
638
+
639
+ if (typeof schema === 'symbol') {
575
640
  const index = context.register(schema);
576
641
  const valueAlias = context.unique('v');
577
642
  const registryAlias = context.unique('r');
643
+ if (mode.fast) {
644
+ return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = ctx.registry[${index}];\nif (typeof ${valueAlias} !== 'symbol' || ${valueAlias} !== ${registryAlias}) { ${fail} }`;
645
+ } else {
646
+ return `const ${valueAlias} = ${valuePath};\nconst ${registryAlias} = ctx.registry[${index}];\nif (typeof ${valueAlias} !== 'symbol') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected symbol\``)} }\nelse if (${valueAlias} !== ${registryAlias}) { ${emit!(`\`Invalid value \${${valueAlias}.toString()}, expected ${schema.toString()}\``)} }`;
647
+ }
648
+ }
578
649
 
579
- return `
580
- const ${valueAlias} = ${valuePath};
581
- const ${registryAlias} = ctx.registry[${index}];
582
- if (typeof ${valueAlias} !== 'symbol') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected symbol\`); }
583
- if (${valueAlias} !== ${registryAlias}) { throw new TypeError(\`Invalid value \${${valueAlias}.toString()} for path "${path}", expected ${schema.toString()}\`); }
584
- `;
585
- } else if (schema === null || schema === undefined) {
650
+ if (schema === null || schema === undefined) {
586
651
  const valueAlias = context.unique('v');
587
- return `
588
- const ${valueAlias} = ${valuePath};
589
- if (${valueAlias} !== null && ${valueAlias} !== undefined ) { throw new TypeError(\`Invalid value \${JSON.stringify(${valueAlias})} for path "${path}", expected nullable\`); }
590
- `;
652
+ if (mode.fast) {
653
+ return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== null && ${valueAlias} !== undefined) { ${fail} }`;
654
+ } else {
655
+ return `const ${valueAlias} = ${valuePath};\nif (${valueAlias} !== null && ${valueAlias} !== undefined) { ${emit!(`\`Invalid value \${String(${valueAlias})}, expected nullable\``)} }`;
656
+ }
657
+ }
658
+
659
+ const valueAlias = context.unique('v');
660
+ if (mode.fast) {
661
+ return `const ${valueAlias} = ${valuePath};\nif (typeof ${valueAlias} !== '${typeof schema}' || ${valueAlias} !== ${toLiteral(schema)}) { ${fail} }`;
591
662
  } else {
592
- const valueAlias = context.unique('v');
593
663
  const value = context.unique('val');
594
- return `
595
- const ${valueAlias} = ${valuePath};
596
- const ${value} = ${JSON.stringify(schema)};
597
- ${codeGenExpectNonError(valueAlias, path)}
598
- if (typeof ${valueAlias} !== '${typeof schema}') { throw new TypeError(\`Invalid type \${typeof ${valueAlias}} for path "${path}", expected ${typeof schema}\`); }
599
- if (${valueAlias} !== ${value}) { throw new TypeError(\`Invalid value \${JSON.stringify(${valueAlias})} for path "${path}", expected ${JSON.stringify(schema)}\`); }
600
- `;
664
+ return `const ${valueAlias} = ${valuePath};\nconst ${value} = ${toLiteral(schema)};\nif (${valueAlias} instanceof Error) { ${emit!(`\`\${${valueAlias}.message}\``)} }\nelse if (typeof ${valueAlias} !== '${typeof schema}') { ${emit!(`\`Invalid type \${typeof ${valueAlias}}, expected ${typeof schema}\``)} }\nelse if (${valueAlias} !== ${value}) { ${emit!(`\`Invalid value \${String(${valueAlias})}, expected ${toLiteral(schema)}\``)} }`;
601
665
  }
602
666
  };
603
667
 
668
+ const emptyIssues: StandardSchemaV1.Issue[] = [];
669
+
604
670
  /**
605
- * Compiles a schema into a validation function.
671
+ * Validator function returned by compile().
672
+ * Returns true if valid, false if invalid.
673
+ * Access `.issues` property after validation to get error details.
674
+ */
675
+ export interface Validator<T> {
676
+ (data: T): boolean;
677
+ issues: ReadonlyArray<StandardSchemaV1.Issue>;
678
+ }
679
+
680
+ /**
681
+ * Options for the compile function.
682
+ */
683
+ export interface CompileOptions {
684
+ /**
685
+ * When true, collects all validation errors instead of stopping at the first.
686
+ * Default is false (first-error mode) for optimal performance.
687
+ */
688
+ allErrors?: boolean;
689
+ }
690
+
691
+ /**
692
+ * Compiles a schema into a high-performance validation function.
693
+ *
694
+ * By default uses first-error mode which stops at the first validation failure
695
+ * and returns immediately. This provides optimal performance for invalid data.
606
696
  *
607
- * This function takes a schema definition and generates a JavaScript function
608
- * that can be used to validate data against the schema.
697
+ * Set `allErrors: true` to collect all validation errors (slower but more informative).
609
698
  *
610
699
  * @template T - The type of data the schema validates.
611
700
  * @param schema - The schema to compile.
612
- * @param rootName - A name for the root of the data structure (used in error messages).
613
- * @returns A validation function that takes data as input and throws a TypeError if the data does not conform to the schema.
701
+ * @param options - Optional configuration (allErrors: boolean).
702
+ * @returns A validator function that returns boolean. Access `.issues` for error details.
614
703
  *
615
704
  * @example
616
705
  * ```typescript
617
- * import { compile, optional, and, or } from 'ascertain';
706
+ * import { compile, optional, or } from 'ascertain';
618
707
  *
619
708
  * const userSchema = {
620
709
  * name: String,
@@ -623,32 +712,65 @@ if (${valueAlias} !== ${value}) { throw new TypeError(\`Invalid value \${JSON.st
623
712
  * role: or('admin', 'user', 'guest')
624
713
  * };
625
714
  *
626
- * const validateUser = compile(userSchema, 'User');
715
+ * // First-error mode (default) - fastest for invalid data
716
+ * const validate = compile(userSchema);
627
717
  *
628
- * // Valid data - no error thrown
629
- * validateUser({
630
- * name: 'John Doe',
631
- * age: 30,
632
- * email: 'john@example.com',
633
- * role: 'user'
634
- * });
718
+ * // All-errors mode - collects all validation issues
719
+ * const validateAll = compile(userSchema, { allErrors: true });
635
720
  *
636
- * // Invalid data - throws TypeError
637
- * try {
638
- * validateUser({
639
- * name: 123, // Invalid: should be string
640
- * age: 'thirty' // Invalid: should be number
641
- * });
642
- * } catch (error) {
643
- * console.error(error.message); // Detailed validation errors
721
+ * // Valid data
722
+ * if (validate({ name: 'John', age: 30, role: 'user' })) {
723
+ * console.log('Valid!');
724
+ * }
725
+ *
726
+ * // Invalid data - check .issues for details
727
+ * if (!validate({ name: 123, age: 'thirty' })) {
728
+ * console.log(validate.issues); // Array with first validation issue
644
729
  * }
645
730
  * ```
646
731
  */
647
- export const compile = <T>(schema: Schema<T>, rootName: string) => {
732
+ export const compile = <T>(schema: Schema<T>, options?: CompileOptions): Validator<T> => {
733
+ const allErrors = options?.allErrors ?? false;
734
+
735
+ if (allErrors) {
736
+ const fastContext = new Context();
737
+ const fastCode = `${codeGen(schema, fastContext, 'data', { fast: true })}\nreturn true;`;
738
+ const fastValidator = new Function('ctx', `return (data) => {\n${fastCode}\n};`)(fastContext) as (data: T) => boolean;
739
+
740
+ const issueContext = new Context();
741
+ const issueCode = `let issues;\n${codeGen(schema, issueContext, 'data', { fast: false, firstError: false, issues: 'issues', path: [], pathExpr: '[]' })}\nreturn issues || [];`;
742
+ const issueValidator = new Function('ctx', `return (data) => {\n${issueCode}\n};`)(issueContext) as (data: T) => StandardSchemaV1.Issue[];
743
+
744
+ const validator = ((data: T): boolean => {
745
+ if (fastValidator(data)) {
746
+ return true;
747
+ }
748
+ validator.issues = issueValidator(data);
749
+ return false;
750
+ }) as Validator<T>;
751
+ validator.issues = emptyIssues;
752
+ return validator;
753
+ }
754
+
755
+ const fastContext = new Context();
756
+ const fastCode = `${codeGen(schema, fastContext, 'data', { fast: true })}\nreturn true;`;
757
+ const fastValidator = new Function('ctx', `return (data) => {\n${fastCode}\n};`)(fastContext) as (data: T) => boolean;
758
+
648
759
  const context = new Context();
649
- const code = codeGen(schema, context, 'data', rootName);
650
- const validator = new Function('ctx', 'data', code);
651
- return (data: T) => validator(context, data);
760
+ const code = codeGen(schema, context, 'data', { fast: false, firstError: true, issues: 'issues', path: [], pathExpr: '[]' });
761
+ const firstErrorValidator = new Function('ctx', `return (data) => {\nlet issues;\n${code}\nreturn issues;\n};`)(context) as (
762
+ data: T,
763
+ ) => StandardSchemaV1.Issue[] | undefined;
764
+
765
+ const validator = ((data: T): boolean => {
766
+ if (fastValidator(data)) {
767
+ return true;
768
+ }
769
+ validator.issues = firstErrorValidator(data)!;
770
+ return false;
771
+ }) as Validator<T>;
772
+ validator.issues = emptyIssues;
773
+ return validator;
652
774
  };
653
775
 
654
776
  /**
@@ -660,7 +782,6 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
660
782
  * @template T - The type of data the schema validates.
661
783
  * @param schema - The schema to validate against.
662
784
  * @param data - The data to validate.
663
- * @param rootName - A name for the root of the data structure (used in error messages, defaults to '[root]').
664
785
  * @throws `{TypeError}` If the data does not conform to the schema.
665
786
  *
666
787
  * @example
@@ -682,7 +803,7 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
682
803
  * };
683
804
  *
684
805
  * // Validate data - throws if invalid, otherwise continues silently
685
- * ascertain(userSchema, userData, 'UserData');
806
+ * ascertain(userSchema, userData);
686
807
  * console.log('User data is valid!');
687
808
  *
688
809
  * // Example with invalid data
@@ -691,7 +812,7 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
691
812
  * name: 'Bob',
692
813
  * age: 'twenty-five', // Invalid: should be number
693
814
  * active: true
694
- * }, 'UserData');
815
+ * });
695
816
  * } catch (error) {
696
817
  * console.error('Validation failed:', error.message);
697
818
  * }
@@ -703,17 +824,20 @@ export const compile = <T>(schema: Schema<T>, rootName: string) => {
703
824
  * const numbersSchema = [Number];
704
825
  * const numbers = [1, 2, 3, 4, 5];
705
826
  *
706
- * ascertain(numbersSchema, numbers, 'Numbers');
827
+ * ascertain(numbersSchema, numbers);
707
828
  *
708
829
  * // Tuple validation
709
830
  * const coordinateSchema = tuple(Number, Number);
710
831
  * const point = [10, 20];
711
832
  *
712
- * ascertain(coordinateSchema, point, 'Point');
833
+ * ascertain(coordinateSchema, point);
713
834
  * ```
714
835
  */
715
- export const ascertain = <T>(schema: Schema<T>, data: T, rootName = '[root]') => {
716
- compile(schema, rootName)(data);
836
+ export const ascertain = <T>(schema: Schema<T>, data: T) => {
837
+ const validator = compile(schema);
838
+ if (!validator(data)) {
839
+ throw new TypeError(validator.issues[0].message, { cause: { issues: validator.issues } });
840
+ }
717
841
  };
718
842
 
719
843
  /**
@@ -732,7 +856,6 @@ export type ExtractShape<C, S> = {
732
856
  *
733
857
  * @template C - The type of the config object.
734
858
  * @param config - The config object to validate against.
735
- * @param rootName - A name for the root of the data structure (used in error messages).
736
859
  * @returns A validator function that takes a schema and returns the typed config subset.
737
860
  *
738
861
  * @example
@@ -745,7 +868,7 @@ export type ExtractShape<C, S> = {
745
868
  * redis: { host: as.string(process.env.REDIS_HOST) },
746
869
  * };
747
870
  *
748
- * const validate = createValidator(config, '[CONFIG]');
871
+ * const validate = createValidator(config);
749
872
  *
750
873
  * // Consumer only validates what it needs
751
874
  * const { app, kafka } = validate({
@@ -758,9 +881,111 @@ export type ExtractShape<C, S> = {
758
881
  * // redis is not accessible - TypeScript error
759
882
  * ```
760
883
  */
761
- export const createValidator = <C>(config: C, rootName = '[root]') => {
884
+ export const createValidator = <C>(config: C) => {
762
885
  return <S extends Schema<Partial<C>>>(schema: S): ExtractShape<C, S> => {
763
- ascertain(schema as Schema<C>, config, rootName);
886
+ ascertain(schema as Schema<C>, config);
764
887
  return config as ExtractShape<C, S>;
765
888
  };
766
889
  };
890
+
891
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
892
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>;
893
+ }
894
+
895
+ export namespace StandardSchemaV1 {
896
+ export interface Props<Input = unknown, Output = Input> {
897
+ readonly version: 1;
898
+ readonly vendor: string;
899
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
900
+ readonly types?: { readonly input: Input; readonly output: Output };
901
+ }
902
+
903
+ export type Result<Output> = SuccessResult<Output> | FailureResult;
904
+
905
+ export interface SuccessResult<Output> {
906
+ readonly value: Output;
907
+ readonly issues?: undefined;
908
+ }
909
+
910
+ export interface FailureResult {
911
+ readonly issues: ReadonlyArray<Issue>;
912
+ }
913
+
914
+ export interface Issue {
915
+ readonly message: string;
916
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
917
+ }
918
+
919
+ export interface PathSegment {
920
+ readonly key: PropertyKey;
921
+ }
922
+ }
923
+
924
+ /**
925
+ * Wraps an Ascertain schema to make it Standard Schema v1 compliant.
926
+ *
927
+ * Creates a validator that implements the Standard Schema specification,
928
+ * enabling interoperability with tools like tRPC, TanStack Form, and other
929
+ * ecosystem libraries that consume Standard Schema-compliant validators.
930
+ *
931
+ * The returned function can be used both as a regular Ascertain validator
932
+ * (throws on error) and as a Standard Schema validator (returns result object).
933
+ *
934
+ * @template T - The type of data the schema validates.
935
+ * @param schema - The Ascertain schema to wrap.
936
+ * @returns A function that validates data, with a `~standard` property for Standard Schema compliance.
937
+ *
938
+ * @see https://standardschema.dev/
939
+ *
940
+ * @example
941
+ * ```typescript
942
+ * import { standardSchema, or, optional } from 'ascertain';
943
+ *
944
+ * // Create a Standard Schema-compliant validator
945
+ * const userValidator = standardSchema({
946
+ * name: String,
947
+ * age: Number,
948
+ * role: or('admin', 'user'),
949
+ * email: optional(String),
950
+ * });
951
+ *
952
+ * // Use as regular Ascertain validator (throws on error)
953
+ * userValidator({ name: 'Alice', age: 30, role: 'admin' });
954
+ *
955
+ * // Use Standard Schema interface (returns result object)
956
+ * const result = userValidator['~standard'].validate(unknownData);
957
+ * if (result.issues) {
958
+ * console.log(result.issues);
959
+ * } else {
960
+ * console.log(result.value); // typed as User
961
+ * }
962
+ *
963
+ * // Works with tRPC, TanStack Form, etc.
964
+ * ```
965
+ */
966
+ interface StandardSchemaFn<T> {
967
+ (data: T): void;
968
+ '~standard': StandardSchemaV1.Props<T, T>;
969
+ }
970
+
971
+ export const standardSchema = <T>(schema: Schema<T>): StandardSchemaFn<T> => {
972
+ const validator = compile(schema);
973
+
974
+ const fn = ((data: T) => {
975
+ if (!validator(data)) {
976
+ throw new TypeError(validator.issues[0].message, { cause: { issues: validator.issues } });
977
+ }
978
+ }) as StandardSchemaFn<T>;
979
+ fn['~standard'] = {
980
+ version: 1 as const,
981
+ vendor: 'ascertain',
982
+ validate: (value: unknown): StandardSchemaV1.Result<T> => {
983
+ if (validator(value as T)) {
984
+ return { value: value as T };
985
+ }
986
+ return { issues: validator.issues };
987
+ },
988
+ };
989
+
990
+ return fn;
991
+ };