@webergency-utils/typechecker 0.1.9 → 0.2.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.
@@ -11,7 +11,7 @@ export declare function createLiteralCheck(value: string | number | boolean | ts
11
11
  export declare function createArrayCheck(elementValidator: ts.Expression, requiredUtils: Set<string>): ts.Expression;
12
12
  export declare function createTemplateLiteralCheck(regexStr: string, expected: string, requiredUtils: Set<string>): ts.Expression;
13
13
  export declare function createUnionCheck(checks: ts.Expression[], requiredUtils: Set<string>, expected?: string): ts.Expression;
14
- export declare function createObjectCheck(props: any[], requiredUtils: Set<string>, expected?: string): ts.Expression;
14
+ export declare function createObjectCheck(props: any[], requiredUtils: Set<string>, expected?: string, indexValidator?: ts.Expression): ts.Expression;
15
15
  export declare function createRecordCheck(valueValidator: ts.Expression, requiredUtils: Set<string>): ts.Expression;
16
16
  export declare function createTupleCheck(checks: ts.Expression[], requiredUtils: Set<string>): ts.Expression;
17
17
  export declare function createDateCheck(requiredUtils: Set<string>): ts.Expression;
@@ -21,3 +21,4 @@ export declare function createUndefinedCheck(requiredUtils: Set<string>): ts.Exp
21
21
  export declare function createIntersectionCheck(checks: ts.Expression[], requiredUtils: Set<string>): ts.Expression;
22
22
  export declare function createSetCheck(elementValidator: ts.Expression, requiredUtils: Set<string>): ts.Expression;
23
23
  export declare function createMapCheck(keyValidator: ts.Expression, valueValidator: ts.Expression, requiredUtils: Set<string>): ts.Expression;
24
+ export declare function createInstanceOfCheck(typeName: string, requiredUtils: Set<string>): ts.Expression;
@@ -79,7 +79,7 @@ export function createConstrainedPrimitiveCheck(baseType, constraints, requiredU
79
79
  return `validators.pattern(v, path, ctx, new RegExp(${JSON.stringify(c.value)}), ${JSON.stringify('Pattern<' + c.value + '>')}${msgArg})`;
80
80
  }
81
81
  if (c.type === 'format') {
82
- return `validators.format(v, path, ctx, ${JSON.stringify(c.value)}${msgArg})`;
82
+ return `v = validators.format(v, path, ctx, ${JSON.stringify(c.value)}${msgArg})`;
83
83
  }
84
84
  if (c.type === 'minItems') {
85
85
  return `validators.minItems(v, path, ctx, ${valStr}${msgArg})`;
@@ -118,13 +118,13 @@ export function createConstrainedPrimitiveCheck(baseType, constraints, requiredU
118
118
  return 'if (typeof v === \'string\' && v.length > 0) v = v.charAt(0).toUpperCase() + v.slice(1)';
119
119
  }
120
120
  if (tc.type === 'transform' && tc.value === 'tonumber') {
121
- return 'v = Number(v)';
121
+ return 'v = validators.coerceQueryNumber(v)';
122
122
  }
123
123
  if (tc.type === 'transform' && tc.value === 'toboolean') {
124
- return 'v = (v === \'true\' || v === \'1\' || v === true || v === 1)';
124
+ return 'v = validators.coerceQueryBoolean(v)';
125
125
  }
126
126
  if (tc.type === 'transform' && tc.value === 'todate') {
127
- return 'v = new Date(v)';
127
+ return 'v = validators.coerceQueryDate(v)';
128
128
  }
129
129
  if (tc.type === 'transform_custom') {
130
130
  return `v = ${tc.value}(v)`;
@@ -188,19 +188,39 @@ export function createUnionCheck(checks, requiredUtils, expected = 'Type<Union>'
188
188
  ts.factory.createStringLiteral(expected)
189
189
  ]));
190
190
  }
191
- export function createObjectCheck(props, requiredUtils, expected = 'object') {
191
+ export function createObjectCheck(props, requiredUtils, expected = 'object', indexValidator) {
192
192
  requiredUtils.add('validators');
193
- const propDefinitions = props.map((p, i) => ts.factory.createArrayLiteralExpression([
193
+ const propDefinitions = props.map((p) => ts.factory.createArrayLiteralExpression([
194
194
  ts.factory.createStringLiteral(p.name),
195
195
  p.isOptional ? ts.factory.createTrue() : ts.factory.createFalse(),
196
196
  p.validator
197
197
  ]));
198
198
  const allowedKeys = props.map(p => ts.factory.createStringLiteral(p.name));
199
+ if (indexValidator) {
200
+ const tpl = `
201
+ (v, path, ctx) => {
202
+ const obj = validators.object(v, path, ctx, undefined, __EXPECTED__);
203
+ if (obj === false) return v;
204
+ const data = validators.objectShell(obj, ctx);
205
+ validators.props(obj, data, path, ctx, __PROPS__);
206
+ validators.additionalProps(obj, data, path, ctx, __KEYS__, __INDEX__);
207
+ return data;
208
+ }
209
+ `;
210
+ return injectNodes(templateToAst(tpl), {
211
+ '__KEYS__': ts.factory.createArrayLiteralExpression(allowedKeys),
212
+ '__EXPECTED__': ts.factory.createStringLiteral(expected),
213
+ '__PROPS__': ts.factory.createArrayLiteralExpression(propDefinitions, true),
214
+ '__INDEX__': indexValidator
215
+ });
216
+ }
199
217
  const tpl = `
200
218
  (v, path, ctx) => {
201
- if (!validators.object(v, path, ctx, __KEYS__, __EXPECTED__)) return v;
202
- const data = ctx.mode === 'strip' ? {} : v;
203
- validators.props(v, data, path, ctx, __PROPS__);
219
+ const obj = validators.object(v, path, ctx, __KEYS__, __EXPECTED__);
220
+ if (obj === false) return v;
221
+ const data = validators.objectShell(obj, ctx);
222
+ validators.props(obj, data, path, ctx, __PROPS__);
223
+ validators.stripExtras(data, ctx, __KEYS__);
204
224
  return data;
205
225
  }
206
226
  `;
@@ -243,10 +263,11 @@ export function createIntersectionCheck(checks, requiredUtils) {
243
263
  const tpl = `
244
264
  (v, path, ctx) => {
245
265
  const checks = __CHECKS__;
246
- let data = ctx.mode === "strip" ? (typeof v === "object" && v !== null && !Array.isArray(v) ? {} : v) : v;
266
+ let data = validators.objectShell(v, ctx);
247
267
  for (let i = 0; i < checks.length; i++) {
248
268
  const val = checks[i](v, path, ctx);
249
- if (ctx.mode === "strip" && typeof val === "object" && val !== null) Object.assign(data, val);
269
+ if (typeof val === "object" && val !== null && !Array.isArray(val) && typeof data === "object" && data !== null && !Array.isArray(data)) Object.assign(data, val);
270
+ else data = val;
250
271
  }
251
272
  return data;
252
273
  }
@@ -265,3 +286,8 @@ export function createMapCheck(keyValidator, valueValidator, requiredUtils) {
265
286
  const tpl = '(v, path, ctx) => validators.map(v, path, ctx, __KEY__, __VALUE__)';
266
287
  return injectNodes(templateToAst(tpl), { '__KEY__': keyValidator, '__VALUE__': valueValidator });
267
288
  }
289
+ export function createInstanceOfCheck(typeName, requiredUtils) {
290
+ requiredUtils.add('validators');
291
+ const tpl = `(v, path, ctx) => validators.instanceOf(v, path, ctx, ${JSON.stringify(typeName)})`;
292
+ return templateToAst(tpl);
293
+ }
@@ -1,5 +1,5 @@
1
1
  import ts from '../ts.js';
2
- import { createPrimitiveCheck, createLiteralCheck, createArrayCheck, createUnionCheck, createObjectCheck, createDateCheck, createNullCheck, createUndefinedCheck, createIntersectionCheck, createTupleCheck, createRecordCheck, createRegExpCheck, createTemplateLiteralCheck, createConstrainedPrimitiveCheck, createSetCheck, createMapCheck } from './generators.js';
2
+ import { createPrimitiveCheck, createLiteralCheck, createArrayCheck, createUnionCheck, createObjectCheck, createDateCheck, createNullCheck, createUndefinedCheck, createIntersectionCheck, createTupleCheck, createRecordCheck, createRegExpCheck, createTemplateLiteralCheck, createConstrainedPrimitiveCheck, createSetCheck, createMapCheck, createInstanceOfCheck } from './generators.js';
3
3
  import { createHash } from 'crypto';
4
4
  function getStringLiteralValue(type) {
5
5
  if (type.isStringLiteral()) {
@@ -48,6 +48,87 @@ function minifyTypeString(str) {
48
48
  .replace(/:\s+/g, ':')
49
49
  .replace(/\s+\|\s+/g, '|');
50
50
  }
51
+ function isNativeEnumType(type) {
52
+ const flags = type.getFlags();
53
+ if (flags & ts.TypeFlags.Enum) {
54
+ return true;
55
+ }
56
+ const symbol = type.getSymbol();
57
+ if (!symbol) {
58
+ return false;
59
+ }
60
+ return (symbol.flags & (ts.SymbolFlags.RegularEnum | ts.SymbolFlags.ConstEnum)) !== 0;
61
+ }
62
+ function buildEnumValidator(type, checker, validatorsMap, requiredUtils) {
63
+ const symbol = type.getSymbol();
64
+ const checks = [];
65
+ if (symbol?.exports) {
66
+ symbol.exports.forEach(member => {
67
+ if (!(member.flags & ts.SymbolFlags.EnumMember)) {
68
+ return;
69
+ }
70
+ const declaration = member.valueDeclaration || member.declarations?.[0];
71
+ if (!declaration) {
72
+ return;
73
+ }
74
+ const memberType = checker.getTypeOfSymbolAtLocation(member, declaration);
75
+ checks.push(buildValidator(memberType, checker, validatorsMap, requiredUtils));
76
+ });
77
+ }
78
+ if (checks.length === 0 && type.isUnion()) {
79
+ const unionChecks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
80
+ return createUnionCheck(unionChecks, requiredUtils, `Type<${minifyTypeString(checker.typeToString(type))}>`);
81
+ }
82
+ if (checks.length === 0) {
83
+ return createPrimitiveCheck('any', requiredUtils);
84
+ }
85
+ if (checks.length === 1) {
86
+ return checks[0];
87
+ }
88
+ return createUnionCheck(checks, requiredUtils, `Type<${minifyTypeString(checker.typeToString(type))}>`);
89
+ }
90
+ function isConstraintOnlyType(type, checker) {
91
+ const props = checker.getPropertiesOfType(type);
92
+ return props.length > 0 && props.every(p => p.getName().startsWith('__'));
93
+ }
94
+ function tryMergeObjectIntersection(types, checker, validatorsMap, requiredUtils, expected) {
95
+ const objectTypes = types.filter(t => {
96
+ if (isConstraintOnlyType(t, checker)) {
97
+ return false;
98
+ }
99
+ const flags = t.getFlags();
100
+ return ((flags & ts.TypeFlags.Object) !== 0) || t.isClassOrInterface();
101
+ });
102
+ if (objectTypes.length < 2) {
103
+ return undefined;
104
+ }
105
+ const others = types.filter(t => !isConstraintOnlyType(t, checker) && !objectTypes.includes(t));
106
+ if (others.length > 0) {
107
+ return undefined;
108
+ }
109
+ const propMap = new Map();
110
+ let indexValidator;
111
+ for (const t of objectTypes) {
112
+ const stringIndexInfo = checker.getIndexInfoOfType(t, ts.IndexKind.String);
113
+ if (stringIndexInfo) {
114
+ indexValidator = buildValidator(stringIndexInfo.type, checker, validatorsMap, requiredUtils);
115
+ }
116
+ for (const prop of checker.getPropertiesOfType(t)) {
117
+ const name = prop.getName();
118
+ if (name.startsWith('__')) {
119
+ continue;
120
+ }
121
+ const declaration = prop.valueDeclaration || prop.declarations?.[0];
122
+ const propType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
123
+ propMap.set(name, {
124
+ name,
125
+ isOptional: (prop.getFlags() & ts.SymbolFlags.Optional) !== 0,
126
+ validator: buildValidator(propType, checker, validatorsMap, requiredUtils)
127
+ });
128
+ }
129
+ }
130
+ return createObjectCheck([...propMap.values()], requiredUtils, expected, indexValidator);
131
+ }
51
132
  export function buildValidator(type, checker, validatorsMap, requiredUtils) {
52
133
  const hash = generateHash(type, checker);
53
134
  if (validatorsMap.has(hash)) {
@@ -159,7 +240,7 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
159
240
  }
160
241
  }
161
242
  if (fnName === '__function' || !fnName) {
162
- throw new Error('[Webergency] Custom validator must reference a named function via typeof (e.g. tag.Custom<typeof myFunc>).');
243
+ throw new Error('[Webergency] Custom transform must reference a named function via typeof (e.g. transform.Custom<typeof myFunc>).');
163
244
  }
164
245
  if (filePath) {
165
246
  requiredUtils.add(`custom:${fnName}:${filePath}`);
@@ -199,7 +280,7 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
199
280
  }
200
281
  }
201
282
  if (fnName === '__function' || !fnName) {
202
- throw new Error('[Webergency] Custom validator must reference a named function via typeof (e.g. tag.Custom<typeof myFunc>).');
283
+ throw new Error('[Webergency] Custom validator must reference a named function via typeof (e.g. constraint.Custom<typeof myFunc>).');
203
284
  }
204
285
  if (filePath) {
205
286
  requiredUtils.add(`custom:${fnName}:${filePath}`);
@@ -306,21 +387,27 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
306
387
  baseValidator = buildValidator(nonConstraintTypes[0], checker, validatorsMap, requiredUtils);
307
388
  }
308
389
  else if (nonConstraintTypes.length > 1) {
309
- const checks = nonConstraintTypes.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
310
- baseValidator = createIntersectionCheck(checks, requiredUtils);
390
+ const merged = tryMergeObjectIntersection(nonConstraintTypes, checker, validatorsMap, requiredUtils, minifyTypeString(checker.typeToString(type)));
391
+ baseValidator = merged || createIntersectionCheck(nonConstraintTypes.map(t => buildValidator(t, checker, validatorsMap, requiredUtils)), requiredUtils);
311
392
  }
312
393
  if (baseValidator) {
313
394
  result = createConstrainedPrimitiveCheck('any', constraints, requiredUtils, baseValidator);
314
395
  }
315
396
  else {
316
- const checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
317
- result = createIntersectionCheck(checks, requiredUtils);
397
+ const merged = tryMergeObjectIntersection(types, checker, validatorsMap, requiredUtils, minifyTypeString(checker.typeToString(type)));
398
+ result = merged || createIntersectionCheck(type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils)), requiredUtils);
318
399
  }
319
400
  }
320
401
  }
321
402
  else {
322
- const checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
323
- result = createIntersectionCheck(checks, requiredUtils);
403
+ const merged = tryMergeObjectIntersection(types, checker, validatorsMap, requiredUtils, minifyTypeString(checker.typeToString(type)));
404
+ if (merged) {
405
+ result = merged;
406
+ }
407
+ else {
408
+ const checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
409
+ result = createIntersectionCheck(checks, requiredUtils);
410
+ }
324
411
  }
325
412
  }
326
413
  else if (type.getSymbol()?.name === 'Date') {
@@ -338,6 +425,15 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
338
425
  const valueType = type.typeArguments?.[1] || checker.getAnyType();
339
426
  result = createMapCheck(buildValidator(keyType, checker, validatorsMap, requiredUtils), buildValidator(valueType, checker, validatorsMap, requiredUtils), requiredUtils);
340
427
  }
428
+ else if (type.getSymbol()?.name === 'Promise') {
429
+ result = createInstanceOfCheck('Promise', requiredUtils);
430
+ }
431
+ else if (type.getSymbol()?.name && [
432
+ 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array',
433
+ 'Float32Array', 'Float64Array', 'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Buffer'
434
+ ].includes(type.getSymbol().name)) {
435
+ result = createInstanceOfCheck(type.getSymbol().name, requiredUtils);
436
+ }
341
437
  else if (flags & ts.TypeFlags.Null) {
342
438
  result = createNullCheck(requiredUtils);
343
439
  }
@@ -356,6 +452,12 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
356
452
  else if (flags & ts.TypeFlags.Boolean) {
357
453
  result = createPrimitiveCheck('boolean', requiredUtils);
358
454
  }
455
+ else if (flags & ts.TypeFlags.Never) {
456
+ result = createPrimitiveCheck('never', requiredUtils);
457
+ }
458
+ else if (flags & ts.TypeFlags.ESSymbol || flags & ts.TypeFlags.UniqueESSymbol || type.intrinsicName === 'symbol') {
459
+ result = createPrimitiveCheck('symbol', requiredUtils);
460
+ }
359
461
  else if (flags & ts.TypeFlags.TemplateLiteral) {
360
462
  const templateType = type;
361
463
  let regexStr = '^';
@@ -404,23 +506,32 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
404
506
  const elementType = type.typeArguments?.[0] || checker.getAnyType();
405
507
  result = createArrayCheck(buildValidator(elementType, checker, validatorsMap, requiredUtils), requiredUtils);
406
508
  }
509
+ else if (type.getCallSignatures().length > 0 && type.getConstructSignatures().length === 0) {
510
+ result = createPrimitiveCheck('function', requiredUtils);
511
+ }
512
+ else if (isNativeEnumType(type)) {
513
+ result = buildEnumValidator(type, checker, validatorsMap, requiredUtils);
514
+ }
407
515
  else {
408
516
  const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
409
- if (stringIndexInfo) {
517
+ const props = checker.getPropertiesOfType(type).map(prop => {
518
+ const declaration = prop.valueDeclaration || prop.declarations?.[0];
519
+ const propType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
520
+ return {
521
+ name: prop.getName(),
522
+ isOptional: (prop.getFlags() & ts.SymbolFlags.Optional) !== 0,
523
+ validator: buildValidator(propType, checker, validatorsMap, requiredUtils)
524
+ };
525
+ });
526
+ if (stringIndexInfo && props.length === 0) {
410
527
  result = createRecordCheck(buildValidator(stringIndexInfo.type, checker, validatorsMap, requiredUtils), requiredUtils);
411
528
  }
412
- else if (flags & ts.TypeFlags.Object || type.isClassOrInterface() || type.isTypeParameter()) {
413
- const props = checker.getPropertiesOfType(type).map(prop => {
414
- const declaration = prop.valueDeclaration || prop.declarations?.[0];
415
- const propType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
416
- return {
417
- name: prop.getName(),
418
- isOptional: (prop.getFlags() & ts.SymbolFlags.Optional) !== 0,
419
- validator: buildValidator(propType, checker, validatorsMap, requiredUtils)
420
- };
421
- });
529
+ else if (flags & ts.TypeFlags.Object || type.isClassOrInterface() || type.isTypeParameter() || stringIndexInfo) {
422
530
  const typeName = checker.typeToString(type);
423
- result = createObjectCheck(props, requiredUtils, typeName);
531
+ const indexValidator = stringIndexInfo
532
+ ? buildValidator(stringIndexInfo.type, checker, validatorsMap, requiredUtils)
533
+ : undefined;
534
+ result = createObjectCheck(props, requiredUtils, typeName, indexValidator);
424
535
  }
425
536
  else {
426
537
  result = createPrimitiveCheck('any', requiredUtils);
@@ -475,6 +586,38 @@ function buildStructuralSignature(type, checker, visited = new Set()) {
475
586
  if (flags & ts.TypeFlags.Undefined || flags & ts.TypeFlags.Void) {
476
587
  return 'undefined';
477
588
  }
589
+ if (flags & ts.TypeFlags.Never) {
590
+ return 'never';
591
+ }
592
+ if (flags & ts.TypeFlags.Unknown) {
593
+ return 'unknown';
594
+ }
595
+ if (flags & ts.TypeFlags.Any) {
596
+ return 'any';
597
+ }
598
+ if (flags & ts.TypeFlags.ESSymbol || flags & ts.TypeFlags.UniqueESSymbol || type.intrinsicName === 'symbol') {
599
+ return 'symbol';
600
+ }
601
+ if (type.getCallSignatures().length > 0 && type.getConstructSignatures().length === 0) {
602
+ return 'function';
603
+ }
604
+ if (isNativeEnumType(type)) {
605
+ const symbol = type.getSymbol();
606
+ const members = [];
607
+ if (symbol?.exports) {
608
+ symbol.exports.forEach(member => {
609
+ if (!(member.flags & ts.SymbolFlags.EnumMember)) {
610
+ return;
611
+ }
612
+ const declaration = member.valueDeclaration || member.declarations?.[0];
613
+ if (!declaration) {
614
+ return;
615
+ }
616
+ members.push(buildStructuralSignature(checker.getTypeOfSymbolAtLocation(member, declaration), checker, visited));
617
+ });
618
+ }
619
+ return `Enum<${symbol?.name || 'anonymous'}:${members.sort().join(',')}>`;
620
+ }
478
621
  if (flags & ts.TypeFlags.TemplateLiteral) {
479
622
  const templateType = type;
480
623
  return `TemplateLiteral<${templateType.texts.join(',')}|${templateType.types.map(t => buildStructuralSignature(t, checker, visited)).join(',')}>`;
@@ -483,11 +626,37 @@ function buildStructuralSignature(type, checker, visited = new Set()) {
483
626
  const elementType = type.typeArguments?.[0] || checker.getAnyType();
484
627
  return `Array<${buildStructuralSignature(elementType, checker, visited)}>`;
485
628
  }
629
+ const typeSymbolName = type.getSymbol()?.name || type.aliasSymbol?.getName();
630
+ if (typeSymbolName === 'Date') {
631
+ return 'Date';
632
+ }
633
+ if (typeSymbolName === 'RegExp') {
634
+ return 'RegExp';
635
+ }
636
+ if (typeSymbolName === 'Promise') {
637
+ const valueType = type.typeArguments?.[0] || checker.getAnyType();
638
+ return `Promise<${buildStructuralSignature(valueType, checker, visited)}>`;
639
+ }
640
+ if (typeSymbolName === 'Set') {
641
+ const elementType = type.typeArguments?.[0] || checker.getAnyType();
642
+ return `Set<${buildStructuralSignature(elementType, checker, visited)}>`;
643
+ }
644
+ if (typeSymbolName === 'Map') {
645
+ const keyType = type.typeArguments?.[0] || checker.getAnyType();
646
+ const valueType = type.typeArguments?.[1] || checker.getAnyType();
647
+ return `Map<${buildStructuralSignature(keyType, checker, visited)},${buildStructuralSignature(valueType, checker, visited)}>`;
648
+ }
649
+ if (typeSymbolName && [
650
+ 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array',
651
+ 'Float32Array', 'Float64Array', 'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Buffer'
652
+ ].includes(typeSymbolName)) {
653
+ return typeSymbolName;
654
+ }
486
655
  if (flags & ts.TypeFlags.Object || type.isClassOrInterface()) {
487
656
  const props = checker.getPropertiesOfType(type);
657
+ const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
488
658
  if (props.length === 0) {
489
659
  // Handle Record or empty object
490
- const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
491
660
  if (stringIndexInfo) {
492
661
  return `Record<${buildStructuralSignature(stringIndexInfo.type, checker, visited)}>`;
493
662
  }
@@ -498,7 +667,10 @@ function buildStructuralSignature(type, checker, visited = new Set()) {
498
667
  const isOptional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
499
668
  return `${prop.getName()}${isOptional ? '?' : ''}:${buildStructuralSignature(propType, checker, visited)}`;
500
669
  }).sort();
501
- return `Object{${propSigs.join(';')}}`;
670
+ const indexSig = stringIndexInfo
671
+ ? `;[string]:${buildStructuralSignature(stringIndexInfo.type, checker, visited)}`
672
+ : '';
673
+ return `Object{${propSigs.join(';')}${indexSig}}`;
502
674
  }
503
675
  return 'any';
504
676
  }
@@ -679,42 +851,14 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
679
851
  const types = type.types;
680
852
  let baseSchema = {};
681
853
  const constraints = {};
854
+ const memberSchemas = [];
682
855
  for (const sub of types) {
683
856
  const sFlags = sub.getFlags();
684
- if (sFlags & ts.TypeFlags.String || sFlags & ts.TypeFlags.TemplateLiteral) {
685
- baseSchema = { type: 'string' };
686
- }
687
- else if (sFlags & ts.TypeFlags.Number) {
688
- baseSchema = { type: 'number' };
689
- }
690
- else if (sFlags & ts.TypeFlags.BigInt) {
691
- baseSchema = { type: 'integer' };
692
- }
693
- else if (sFlags & ts.TypeFlags.Boolean || sub.intrinsicName === 'boolean') {
694
- baseSchema = { type: 'boolean' };
695
- }
696
- else if (sub.getSymbol()?.name === 'Date') {
697
- baseSchema = { type: 'string', format: 'date-time' };
698
- }
699
- else if (sub.getSymbol()?.name === 'RegExp') {
700
- baseSchema = { type: 'string', format: 'regex' };
701
- }
702
- else if (sub.getSymbol()?.name === 'Set') {
703
- const elementType = sub.typeArguments?.[0] || checker.getAnyType();
704
- baseSchema = { type: 'array', items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes), uniqueItems: true };
705
- }
706
- else if (sub.getSymbol()?.name === 'Map') {
707
- const valueType = sub.typeArguments?.[1] || checker.getAnyType();
708
- baseSchema = { type: 'object', additionalProperties: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes) };
709
- }
710
- else if (checker.isArrayType(sub)) {
711
- const elementType = sub.typeArguments?.[0] || checker.getAnyType();
712
- baseSchema = { type: 'array', items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes) };
713
- }
714
- const props = checker.getPropertiesOfType(sub);
715
- for (const prop of props) {
716
- const pName = prop.getName();
717
- if (pName.startsWith('__')) {
857
+ const subProps = checker.getPropertiesOfType(sub);
858
+ const isConstraintPhantom = subProps.length > 0 && subProps.every(p => p.getName().startsWith('__'));
859
+ if (isConstraintPhantom) {
860
+ for (const prop of subProps) {
861
+ const pName = prop.getName();
718
862
  const pType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
719
863
  const actualType = stripUndefinedFromType(pType);
720
864
  const val = getTagPropertyValue(pType);
@@ -775,36 +919,97 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
775
919
  constraints.requires = reqVal;
776
920
  }
777
921
  }
922
+ continue;
923
+ }
924
+ if (sFlags & ts.TypeFlags.String || sFlags & ts.TypeFlags.TemplateLiteral) {
925
+ baseSchema = { type: 'string' };
926
+ }
927
+ else if (sFlags & ts.TypeFlags.Number) {
928
+ baseSchema = { type: 'number' };
929
+ }
930
+ else if (sFlags & ts.TypeFlags.BigInt) {
931
+ baseSchema = { 'x-typescript-type': 'bigint' };
778
932
  }
933
+ else if (sFlags & ts.TypeFlags.Boolean || sub.intrinsicName === 'boolean') {
934
+ baseSchema = { type: 'boolean' };
935
+ }
936
+ else if (sub.getSymbol()?.name === 'Date') {
937
+ baseSchema = { 'x-typescript-type': 'Date' };
938
+ }
939
+ else if (sub.getSymbol()?.name === 'RegExp') {
940
+ baseSchema = { 'x-typescript-type': 'RegExp' };
941
+ }
942
+ else if (sub.getSymbol()?.name === 'Set') {
943
+ const elementType = sub.typeArguments?.[0] || checker.getAnyType();
944
+ baseSchema = {
945
+ 'x-typescript-type': 'Set',
946
+ items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes)
947
+ };
948
+ }
949
+ else if (sub.getSymbol()?.name === 'Map') {
950
+ const keyType = sub.typeArguments?.[0] || checker.getAnyType();
951
+ const valueType = sub.typeArguments?.[1] || checker.getAnyType();
952
+ baseSchema = {
953
+ 'x-typescript-type': 'Map',
954
+ key: buildJsonSchemaInternal(keyType, checker, defs, visited, counts, circularHashes),
955
+ value: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes)
956
+ };
957
+ }
958
+ else if (checker.isArrayType(sub)) {
959
+ const elementType = sub.typeArguments?.[0] || checker.getAnyType();
960
+ baseSchema = { type: 'array', items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes) };
961
+ }
962
+ else if ((sFlags & ts.TypeFlags.Object) || sub.isClassOrInterface()) {
963
+ memberSchemas.push(buildJsonSchemaInternal(sub, checker, defs, visited, counts, circularHashes));
964
+ }
965
+ }
966
+ if (memberSchemas.length > 1) {
967
+ return { allOf: memberSchemas, ...constraints };
968
+ }
969
+ if (memberSchemas.length === 1) {
970
+ return { ...memberSchemas[0], ...constraints };
779
971
  }
780
972
  return { ...baseSchema, ...constraints };
781
973
  }
782
974
  if (type.getSymbol()?.name === 'Date') {
783
- return { type: 'string', format: 'date-time' };
975
+ return { 'x-typescript-type': 'Date' };
784
976
  }
785
977
  if (type.getSymbol()?.name === 'RegExp') {
786
- return { type: 'string', format: 'regex' };
978
+ return { 'x-typescript-type': 'RegExp' };
979
+ }
980
+ if (type.getSymbol()?.name === 'Promise') {
981
+ return { 'x-typescript-type': 'Promise' };
982
+ }
983
+ {
984
+ const typedName = type.getSymbol()?.name;
985
+ if (typedName && [
986
+ 'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array',
987
+ 'Float32Array', 'Float64Array', 'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Buffer'
988
+ ].includes(typedName)) {
989
+ return { 'x-typescript-type': typedName };
990
+ }
787
991
  }
788
992
  if (type.getSymbol()?.name === 'Set') {
789
993
  const elementType = type.typeArguments?.[0] || checker.getAnyType();
790
994
  return {
791
- type: 'array',
792
- items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes),
793
- uniqueItems: true
995
+ 'x-typescript-type': 'Set',
996
+ items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes)
794
997
  };
795
998
  }
796
999
  if (type.getSymbol()?.name === 'Map') {
1000
+ const keyType = type.typeArguments?.[0] || checker.getAnyType();
797
1001
  const valueType = type.typeArguments?.[1] || checker.getAnyType();
798
1002
  return {
799
- type: 'object',
800
- additionalProperties: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes)
1003
+ 'x-typescript-type': 'Map',
1004
+ key: buildJsonSchemaInternal(keyType, checker, defs, visited, counts, circularHashes),
1005
+ value: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes)
801
1006
  };
802
1007
  }
803
1008
  if (flags & ts.TypeFlags.Null) {
804
1009
  return { type: 'null' };
805
1010
  }
806
1011
  if (flags & ts.TypeFlags.Undefined || flags & ts.TypeFlags.Void) {
807
- return { type: 'null', description: 'undefined' };
1012
+ return { 'x-typescript-type': 'undefined' };
808
1013
  }
809
1014
  if (flags & ts.TypeFlags.String) {
810
1015
  return { type: 'string' };
@@ -813,7 +1018,7 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
813
1018
  return { type: 'number' };
814
1019
  }
815
1020
  if (flags & ts.TypeFlags.BigInt) {
816
- return { type: 'integer' };
1021
+ return { 'x-typescript-type': 'bigint' };
817
1022
  }
818
1023
  if (flags & ts.TypeFlags.Boolean || type.intrinsicName === 'boolean') {
819
1024
  return { type: 'boolean' };
@@ -848,6 +1053,7 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
848
1053
  }
849
1054
  // Object types
850
1055
  if (flags & ts.TypeFlags.Object || type.isClassOrInterface()) {
1056
+ const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
851
1057
  const symbol = type.getSymbol() || type.aliasSymbol;
852
1058
  const name = symbol ? symbol.getName() : 'Object';
853
1059
  const typeHash = generateHash(type, checker);
@@ -875,9 +1081,14 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
875
1081
  }
876
1082
  const schemaObj = {
877
1083
  type: 'object',
878
- properties,
879
- additionalProperties: false
1084
+ properties
880
1085
  };
1086
+ if (stringIndexInfo) {
1087
+ schemaObj.additionalProperties = buildJsonSchemaInternal(stringIndexInfo.type, checker, defs, visited, counts, circularHashes);
1088
+ }
1089
+ else {
1090
+ schemaObj.additionalProperties = false;
1091
+ }
881
1092
  if (required.length > 0) {
882
1093
  schemaObj.required = required;
883
1094
  }
@@ -168,12 +168,13 @@ export function evaluateStaticConstraints(constant, constraints) {
168
168
  else if (c.type === 'multipleOf' && constant.kind === 'number') {
169
169
  const v = constant.value;
170
170
  const n = c.value;
171
- let ok = true;
171
+ let ok;
172
172
  if (typeof v === 'bigint' || typeof n === 'bigint') {
173
173
  ok = BigInt(v) % BigInt(n) === 0n;
174
174
  }
175
175
  else {
176
- ok = v % n === 0;
176
+ const q = v / n;
177
+ ok = Math.abs(q - Math.round(q)) <= 1e-8 * Math.max(1, Math.abs(q));
177
178
  }
178
179
  if (!ok) {
179
180
  errors.push(c.message || `Value ${String(v)} does not satisfy MultipleOf<${n}>`);
package/dist/index.d.ts CHANGED
@@ -1,21 +1,27 @@
1
1
  import { ResolveDefaults } from './runtime/tags.js';
2
+ import type { IValidationError } from './runtime/validators.js';
2
3
  export interface IValidation<T> {
3
4
  success: boolean;
4
5
  data?: T;
5
- errors?: any[];
6
+ errors?: IValidationError[];
6
7
  }
7
8
  export type ValidationMode = 'strict' | 'relaxed' | 'strip';
8
9
  export interface ValidationOptions {
9
10
  mode?: ValidationMode;
10
- tryConvert?: boolean;
11
+ from?: 'json' | 'query' | ((key: string, value: any, type: 'string' | 'number' | 'boolean' | 'bigint' | 'function' | 'symbol' | 'never' | 'Date' | 'RegExp' | 'Set' | 'Map' | 'Array' | 'Object' | 'instance' | 'null' | 'undefined' | 'tuple' | 'literal') => any);
11
12
  wrapArrays?: boolean;
13
+ /** When true, write validated/coerced values onto the input. Default false: always return new containers. */
14
+ mutate?: boolean;
12
15
  schema?: any;
13
16
  errorFactory?: (errors: any[]) => Error;
14
17
  }
15
- export declare function is<T>(input: unknown, options?: ValidationMode | ValidationOptions): input is ResolveDefaults<T>;
16
- export declare function assert<T>(input: unknown, options?: ValidationMode | ValidationOptions): ResolveDefaults<T>;
17
- export declare function assertGuard<T>(input: unknown, options?: ValidationMode | ValidationOptions): asserts input is ResolveDefaults<T>;
18
- export declare function validate<T>(input: unknown, options?: ValidationMode | ValidationOptions): IValidation<ResolveDefaults<T>>;
18
+ /** Returns whether `input` already matches `T`. Does not coerce; `from` is ignored. */
19
+ export declare function is<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): _input is ResolveDefaults<T>;
20
+ /** Validates and returns the (possibly coerced) value. Use `from` when conversion is needed. */
21
+ export declare function assert<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): ResolveDefaults<T>;
22
+ /** Asserts `input` already matches `T`. Does not coerce; `from` is ignored. */
23
+ export declare function assertGuard<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): asserts _input is ResolveDefaults<T>;
24
+ export declare function validate<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): IValidation<ResolveDefaults<T>>;
19
25
  export declare function jsonSchema<T>(): any;
20
26
  export * from './runtime/validators.js';
21
27
  export * from './runtime/tags.js';