@tldraw/validate 4.2.2 → 4.2.3

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.
@@ -9,9 +9,6 @@ import {
9
9
  validateIndexKey,
10
10
  } from '@tldraw/utils'
11
11
 
12
- /** @internal */
13
- const IS_DEV = process.env.NODE_ENV !== 'production'
14
-
15
12
  /**
16
13
  * A function that validates and returns a value of type T from unknown input.
17
14
  * The function should throw a ValidationError if the value is invalid.
@@ -234,13 +231,10 @@ export class Validator<T> implements Validatable<T> {
234
231
  *
235
232
  * validationFn - Function that validates and returns a value of type T
236
233
  * validateUsingKnownGoodVersionFn - Optional performance-optimized validation function
237
- * skipSameValueCheck - Internal flag to skip dev check for validators that transform values
238
234
  */
239
235
  constructor(
240
236
  readonly validationFn: ValidatorFn<T>,
241
- readonly validateUsingKnownGoodVersionFn?: ValidatorUsingKnownGoodVersionFn<T>,
242
- /** @internal */
243
- readonly skipSameValueCheck: boolean = false
237
+ readonly validateUsingKnownGoodVersionFn?: ValidatorUsingKnownGoodVersionFn<T>
244
238
  ) {}
245
239
 
246
240
  /**
@@ -265,7 +259,7 @@ export class Validator<T> implements Validatable<T> {
265
259
  */
266
260
  validate(value: unknown): T {
267
261
  const validated = this.validationFn(value)
268
- if (IS_DEV && !this.skipSameValueCheck && !Object.is(value, validated)) {
262
+ if (process.env.NODE_ENV !== 'production' && !Object.is(value, validated)) {
269
263
  throw new ValidationError('Validator functions must return the same value they were passed')
270
264
  }
271
265
  return validated
@@ -434,8 +428,7 @@ export class Validator<T> implements Validatable<T> {
434
428
  return knownGoodValue
435
429
  }
436
430
  return otherValidationFn(validated)
437
- },
438
- true // skipSameValueCheck: refine is designed to transform values
431
+ }
439
432
  )
440
433
  }
441
434
 
@@ -523,27 +516,11 @@ export class ArrayOfValidator<T> extends Validator<T[]> {
523
516
  (value) => {
524
517
  const arr = array.validate(value)
525
518
  for (let i = 0; i < arr.length; i++) {
526
- if (IS_DEV) {
527
- prefixError(i, () => itemValidator.validate(arr[i]))
528
- } else {
529
- // Production: inline error handling to avoid closure overhead
530
- try {
531
- itemValidator.validate(arr[i])
532
- } catch (err) {
533
- if (err instanceof ValidationError) {
534
- throw new ValidationError(err.rawMessage, [i, ...err.path])
535
- }
536
- throw new ValidationError((err as Error).toString(), [i])
537
- }
538
- }
519
+ prefixError(i, () => itemValidator.validate(arr[i]))
539
520
  }
540
521
  return arr as T[]
541
522
  },
542
523
  (knownGoodValue, newValue) => {
543
- // Fast path: reference equality means no changes
544
- if (Object.is(knownGoodValue, newValue)) {
545
- return knownGoodValue
546
- }
547
524
  if (!itemValidator.validateUsingKnownGoodVersion) return this.validate(newValue)
548
525
  const arr = array.validate(newValue)
549
526
  let isDifferent = knownGoodValue.length !== arr.length
@@ -551,46 +528,18 @@ export class ArrayOfValidator<T> extends Validator<T[]> {
551
528
  const item = arr[i]
552
529
  if (i >= knownGoodValue.length) {
553
530
  isDifferent = true
554
- if (IS_DEV) {
555
- prefixError(i, () => itemValidator.validate(item))
556
- } else {
557
- try {
558
- itemValidator.validate(item)
559
- } catch (err) {
560
- if (err instanceof ValidationError) {
561
- throw new ValidationError(err.rawMessage, [i, ...err.path])
562
- }
563
- throw new ValidationError((err as Error).toString(), [i])
564
- }
565
- }
531
+ prefixError(i, () => itemValidator.validate(item))
566
532
  continue
567
533
  }
568
534
  // sneaky quick check here to avoid the prefix + validator overhead
569
535
  if (Object.is(knownGoodValue[i], item)) {
570
536
  continue
571
537
  }
572
- if (IS_DEV) {
573
- const checkedItem = prefixError(i, () =>
574
- itemValidator.validateUsingKnownGoodVersion!(knownGoodValue[i], item)
575
- )
576
- if (!Object.is(checkedItem, knownGoodValue[i])) {
577
- isDifferent = true
578
- }
579
- } else {
580
- try {
581
- const checkedItem = itemValidator.validateUsingKnownGoodVersion!(
582
- knownGoodValue[i],
583
- item
584
- )
585
- if (!Object.is(checkedItem, knownGoodValue[i])) {
586
- isDifferent = true
587
- }
588
- } catch (err) {
589
- if (err instanceof ValidationError) {
590
- throw new ValidationError(err.rawMessage, [i, ...err.path])
591
- }
592
- throw new ValidationError((err as Error).toString(), [i])
593
- }
538
+ const checkedItem = prefixError(i, () =>
539
+ itemValidator.validateUsingKnownGoodVersion!(knownGoodValue[i], item)
540
+ )
541
+ if (!Object.is(checkedItem, knownGoodValue[i])) {
542
+ isDifferent = true
594
543
  }
595
544
  }
596
545
 
@@ -679,24 +628,10 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
679
628
  throw new ValidationError(`Expected object, got ${typeToString(object)}`)
680
629
  }
681
630
 
682
- for (const key in config) {
683
- if (!hasOwnProperty(config, key)) continue
684
- const validator = config[key as keyof typeof config]
685
- if (IS_DEV) {
686
- prefixError(key, () => {
687
- ;(validator as Validatable<unknown>).validate(getOwnProperty(object, key))
688
- })
689
- } else {
690
- // Production: inline error handling to avoid closure overhead
691
- try {
692
- ;(validator as Validatable<unknown>).validate(getOwnProperty(object, key))
693
- } catch (err) {
694
- if (err instanceof ValidationError) {
695
- throw new ValidationError(err.rawMessage, [key, ...err.path])
696
- }
697
- throw new ValidationError((err as Error).toString(), [key])
698
- }
699
- }
631
+ for (const [key, validator] of Object.entries(config)) {
632
+ prefixError(key, () => {
633
+ ;(validator as Validatable<unknown>).validate(getOwnProperty(object, key))
634
+ })
700
635
  }
701
636
 
702
637
  if (!shouldAllowUnknownProperties) {
@@ -710,52 +645,29 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
710
645
  return object as Shape
711
646
  },
712
647
  (knownGoodValue, newValue) => {
713
- // Fast path: reference equality means no changes
714
- if (Object.is(knownGoodValue, newValue)) {
715
- return knownGoodValue
716
- }
717
648
  if (typeof newValue !== 'object' || newValue === null) {
718
649
  throw new ValidationError(`Expected object, got ${typeToString(newValue)}`)
719
650
  }
720
651
 
721
652
  let isDifferent = false
722
653
 
723
- for (const key in config) {
724
- if (!hasOwnProperty(config, key)) continue
725
- const validator = config[key as keyof typeof config]
654
+ for (const [key, validator] of Object.entries(config)) {
726
655
  const prev = getOwnProperty(knownGoodValue, key)
727
656
  const next = getOwnProperty(newValue, key)
728
657
  // sneaky quick check here to avoid the prefix + validator overhead
729
658
  if (Object.is(prev, next)) {
730
659
  continue
731
660
  }
732
- if (IS_DEV) {
733
- const checked = prefixError(key, () => {
734
- const validatable = validator as Validatable<unknown>
735
- if (validatable.validateUsingKnownGoodVersion) {
736
- return validatable.validateUsingKnownGoodVersion(prev, next)
737
- } else {
738
- return validatable.validate(next)
739
- }
740
- })
741
- if (!Object.is(checked, prev)) {
742
- isDifferent = true
743
- }
744
- } else {
745
- try {
746
- const validatable = validator as Validatable<unknown>
747
- const checked = validatable.validateUsingKnownGoodVersion
748
- ? validatable.validateUsingKnownGoodVersion(prev, next)
749
- : validatable.validate(next)
750
- if (!Object.is(checked, prev)) {
751
- isDifferent = true
752
- }
753
- } catch (err) {
754
- if (err instanceof ValidationError) {
755
- throw new ValidationError(err.rawMessage, [key, ...err.path])
756
- }
757
- throw new ValidationError((err as Error).toString(), [key])
661
+ const checked = prefixError(key, () => {
662
+ const validatable = validator as Validatable<unknown>
663
+ if (validatable.validateUsingKnownGoodVersion) {
664
+ return validatable.validateUsingKnownGoodVersion(prev, next)
665
+ } else {
666
+ return validatable.validate(next)
758
667
  }
668
+ })
669
+ if (!Object.is(checked, prev)) {
670
+ isDifferent = true
759
671
  }
760
672
  }
761
673
 
@@ -882,7 +794,6 @@ export class UnionValidator<
882
794
  return prefixError(`(${key} = ${variant})`, () => matchingSchema.validate(input))
883
795
  },
884
796
  (prevValue, newValue) => {
885
- // Note: Object.is check is already done by base Validator class
886
797
  this.expectObject(newValue)
887
798
  this.expectObject(prevValue)
888
799
 
@@ -922,15 +833,8 @@ export class UnionValidator<
922
833
  throw new ValidationError(
923
834
  `Expected a string for key "${this.key}", got ${typeToString(variant)}`
924
835
  )
925
- } else if (this.useNumberKeys) {
926
- // Fast finite number check: numVariant - numVariant === 0 is false for Infinity and NaN
927
- // This avoids Number.isFinite function call overhead
928
- const numVariant = Number(variant)
929
- if (numVariant - numVariant !== 0) {
930
- throw new ValidationError(
931
- `Expected a number for key "${this.key}", got "${variant as any}"`
932
- )
933
- }
836
+ } else if (this.useNumberKeys && !Number.isFinite(Number(variant))) {
837
+ throw new ValidationError(`Expected a number for key "${this.key}", got "${variant as any}"`)
934
838
  }
935
839
 
936
840
  const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : undefined
@@ -991,26 +895,11 @@ export class DictValidator<Key extends string, Value> extends Validator<Record<K
991
895
  throw new ValidationError(`Expected object, got ${typeToString(object)}`)
992
896
  }
993
897
 
994
- // Use for...in instead of Object.entries() to avoid array allocation
995
- for (const key in object) {
996
- if (!hasOwnProperty(object, key)) continue
997
- if (IS_DEV) {
998
- prefixError(key, () => {
999
- keyValidator.validate(key)
1000
- valueValidator.validate((object as Record<string, unknown>)[key])
1001
- })
1002
- } else {
1003
- // Production: inline error handling to avoid closure overhead
1004
- try {
1005
- keyValidator.validate(key)
1006
- valueValidator.validate((object as Record<string, unknown>)[key])
1007
- } catch (err) {
1008
- if (err instanceof ValidationError) {
1009
- throw new ValidationError(err.rawMessage, [key, ...err.path])
1010
- }
1011
- throw new ValidationError((err as Error).toString(), [key])
1012
- }
1013
- }
898
+ for (const [key, value] of Object.entries(object)) {
899
+ prefixError(key, () => {
900
+ keyValidator.validate(key)
901
+ valueValidator.validate(value)
902
+ })
1014
903
  }
1015
904
 
1016
905
  return object as Record<Key, Value>
@@ -1020,84 +909,39 @@ export class DictValidator<Key extends string, Value> extends Validator<Record<K
1020
909
  throw new ValidationError(`Expected object, got ${typeToString(newValue)}`)
1021
910
  }
1022
911
 
1023
- const newObj = newValue as Record<string, unknown>
1024
912
  let isDifferent = false
1025
- let newKeyCount = 0
1026
-
1027
- // Use for...in instead of Object.entries() to avoid array allocation
1028
- for (const key in newObj) {
1029
- if (!hasOwnProperty(newObj, key)) continue
1030
- newKeyCount++
1031
-
1032
- const next = newObj[key]
1033
913
 
914
+ for (const [key, value] of Object.entries(newValue)) {
1034
915
  if (!hasOwnProperty(knownGoodValue, key)) {
1035
916
  isDifferent = true
1036
- if (IS_DEV) {
1037
- prefixError(key, () => {
1038
- keyValidator.validate(key)
1039
- valueValidator.validate(next)
1040
- })
1041
- } else {
1042
- try {
1043
- keyValidator.validate(key)
1044
- valueValidator.validate(next)
1045
- } catch (err) {
1046
- if (err instanceof ValidationError) {
1047
- throw new ValidationError(err.rawMessage, [key, ...err.path])
1048
- }
1049
- throw new ValidationError((err as Error).toString(), [key])
1050
- }
1051
- }
917
+ prefixError(key, () => {
918
+ keyValidator.validate(key)
919
+ valueValidator.validate(value)
920
+ })
1052
921
  continue
1053
922
  }
1054
-
1055
- const prev = (knownGoodValue as Record<string, unknown>)[key]
1056
-
1057
- // Quick reference equality check to avoid validator overhead
923
+ const prev = getOwnProperty(knownGoodValue, key)
924
+ const next = value
925
+ // sneaky quick check here to avoid the prefix + validator overhead
1058
926
  if (Object.is(prev, next)) {
1059
927
  continue
1060
928
  }
1061
-
1062
- if (IS_DEV) {
1063
- const checked = prefixError(key, () => {
1064
- if (valueValidator.validateUsingKnownGoodVersion) {
1065
- return valueValidator.validateUsingKnownGoodVersion(prev as Value, next)
1066
- } else {
1067
- return valueValidator.validate(next)
1068
- }
1069
- })
1070
- if (!Object.is(checked, prev)) {
1071
- isDifferent = true
1072
- }
1073
- } else {
1074
- try {
1075
- const checked = valueValidator.validateUsingKnownGoodVersion
1076
- ? valueValidator.validateUsingKnownGoodVersion(prev as Value, next)
1077
- : valueValidator.validate(next)
1078
- if (!Object.is(checked, prev)) {
1079
- isDifferent = true
1080
- }
1081
- } catch (err) {
1082
- if (err instanceof ValidationError) {
1083
- throw new ValidationError(err.rawMessage, [key, ...err.path])
1084
- }
1085
- throw new ValidationError((err as Error).toString(), [key])
929
+ const checked = prefixError(key, () => {
930
+ if (valueValidator.validateUsingKnownGoodVersion) {
931
+ return valueValidator.validateUsingKnownGoodVersion(prev as any, next)
932
+ } else {
933
+ return valueValidator.validate(next)
1086
934
  }
935
+ })
936
+ if (!Object.is(checked, prev)) {
937
+ isDifferent = true
1087
938
  }
1088
939
  }
1089
940
 
1090
- // Only check for removed keys if counts might differ
1091
- // This avoids iterating over knownGoodValue when no keys were removed
1092
- if (!isDifferent) {
1093
- let oldKeyCount = 0
1094
- for (const key in knownGoodValue) {
1095
- if (hasOwnProperty(knownGoodValue, key)) {
1096
- oldKeyCount++
1097
- }
1098
- }
1099
- if (oldKeyCount !== newKeyCount) {
941
+ for (const key of Object.keys(knownGoodValue)) {
942
+ if (!hasOwnProperty(newValue, key)) {
1100
943
  isDifferent = true
944
+ break
1101
945
  }
1102
946
  }
1103
947
 
@@ -1164,21 +1008,13 @@ export const string = typeofValidator<string>('string')
1164
1008
  * ```
1165
1009
  * @public
1166
1010
  */
1167
- export const number = new Validator<number>((value) => {
1168
- // Fast path: check for valid finite number using arithmetic trick
1169
- // value - value === 0 is false for Infinity and NaN (avoids function call overhead)
1170
- if (Number.isFinite(value)) {
1171
- return value as number
1172
- }
1173
- // Slow path: determine specific error
1174
- if (typeof value !== 'number') {
1175
- throw new ValidationError(`Expected number, got ${typeToString(value)}`)
1176
- }
1177
- // value !== value is true only for NaN (faster than Number.isNaN)
1178
- if (value !== value) {
1011
+ export const number = typeofValidator<number>('number').check((number) => {
1012
+ if (Number.isNaN(number)) {
1179
1013
  throw new ValidationError('Expected a number, got NaN')
1180
1014
  }
1181
- throw new ValidationError(`Expected a finite number, got ${value}`)
1015
+ if (!Number.isFinite(number)) {
1016
+ throw new ValidationError(`Expected a finite number, got ${number}`)
1017
+ }
1182
1018
  })
1183
1019
  /**
1184
1020
  * Validator that ensures a value is a non-negative number (\>= 0).
@@ -1192,20 +1028,8 @@ export const number = new Validator<number>((value) => {
1192
1028
  * ```
1193
1029
  * @public
1194
1030
  */
1195
- export const positiveNumber = new Validator<number>((value) => {
1196
- if (Number.isFinite(value) && (value as number) >= 0) {
1197
- return value as number
1198
- }
1199
- if (typeof value !== 'number') {
1200
- throw new ValidationError(`Expected number, got ${typeToString(value)}`)
1201
- }
1202
- if (value !== value) {
1203
- throw new ValidationError('Expected a number, got NaN')
1204
- }
1205
- if (value < 0) {
1206
- throw new ValidationError(`Expected a positive number, got ${value}`)
1207
- }
1208
- throw new ValidationError(`Expected a finite number, got ${value}`)
1031
+ export const positiveNumber = number.check((value) => {
1032
+ if (value < 0) throw new ValidationError(`Expected a positive number, got ${value}`)
1209
1033
  })
1210
1034
  /**
1211
1035
  * Validator that ensures a value is a positive number (\> 0). Rejects zero and negative numbers.
@@ -1218,73 +1042,8 @@ export const positiveNumber = new Validator<number>((value) => {
1218
1042
  * ```
1219
1043
  * @public
1220
1044
  */
1221
- export const nonZeroNumber = new Validator<number>((value) => {
1222
- if (Number.isFinite(value) && (value as number) > 0) {
1223
- return value as number
1224
- }
1225
- if (typeof value !== 'number') {
1226
- throw new ValidationError(`Expected number, got ${typeToString(value)}`)
1227
- }
1228
- if (value !== value) {
1229
- throw new ValidationError('Expected a number, got NaN')
1230
- }
1231
- if (value <= 0) {
1232
- throw new ValidationError(`Expected a non-zero positive number, got ${value}`)
1233
- }
1234
- throw new ValidationError(`Expected a finite number, got ${value}`)
1235
- })
1236
- /**
1237
- * Validator that ensures a value is a finite, non-zero number. Allows negative numbers.
1238
- * Useful for scale factors that can be negative (for flipping) but not zero.
1239
- *
1240
- * @example
1241
- * ```ts
1242
- * const scale = T.nonZeroFiniteNumber.validate(-1.5) // Returns -1.5 (valid, allows negative)
1243
- * T.nonZeroFiniteNumber.validate(0) // Throws ValidationError: "Expected a non-zero number, got 0"
1244
- * T.nonZeroFiniteNumber.validate(Infinity) // Throws ValidationError
1245
- * ```
1246
- * @public
1247
- */
1248
- export const nonZeroFiniteNumber = new Validator<number>((value) => {
1249
- if (Number.isFinite(value) && (value as number) !== 0) {
1250
- return value as number
1251
- }
1252
- if (typeof value !== 'number') {
1253
- throw new ValidationError(`Expected number, got ${typeToString(value)}`)
1254
- }
1255
- if (value !== value) {
1256
- throw new ValidationError('Expected a number, got NaN')
1257
- }
1258
- if (value === 0) {
1259
- throw new ValidationError(`Expected a non-zero number, got 0`)
1260
- }
1261
- throw new ValidationError(`Expected a finite number, got ${value}`)
1262
- })
1263
- /**
1264
- * Validator that ensures a value is a number in the unit interval [0, 1].
1265
- * Useful for opacity, percentages expressed as decimals, and other normalized values.
1266
- *
1267
- * @example
1268
- * ```ts
1269
- * const opacity = T.unitInterval.validate(0.5) // Returns 0.5
1270
- * T.unitInterval.validate(0) // Returns 0 (valid)
1271
- * T.unitInterval.validate(1) // Returns 1 (valid)
1272
- * T.unitInterval.validate(1.5) // Throws ValidationError
1273
- * T.unitInterval.validate(-0.1) // Throws ValidationError
1274
- * ```
1275
- * @public
1276
- */
1277
- export const unitInterval = new Validator<number>((value) => {
1278
- if (Number.isFinite(value) && (value as number) >= 0 && (value as number) <= 1) {
1279
- return value as number
1280
- }
1281
- if (typeof value !== 'number') {
1282
- throw new ValidationError(`Expected number, got ${typeToString(value)}`)
1283
- }
1284
- if (value !== value) {
1285
- throw new ValidationError('Expected a number, got NaN')
1286
- }
1287
- throw new ValidationError(`Expected a number between 0 and 1, got ${value}`)
1045
+ export const nonZeroNumber = number.check((value) => {
1046
+ if (value <= 0) throw new ValidationError(`Expected a non-zero positive number, got ${value}`)
1288
1047
  })
1289
1048
  /**
1290
1049
  * Validator that ensures a value is an integer (whole number).
@@ -1297,21 +1056,8 @@ export const unitInterval = new Validator<number>((value) => {
1297
1056
  * ```
1298
1057
  * @public
1299
1058
  */
1300
- export const integer = new Validator<number>((value) => {
1301
- // Fast path: Number.isInteger checks typeof, finiteness, and integrality in one call
1302
- if (Number.isInteger(value)) {
1303
- return value as number
1304
- }
1305
- if (typeof value !== 'number') {
1306
- throw new ValidationError(`Expected number, got ${typeToString(value)}`)
1307
- }
1308
- if (value !== value) {
1309
- throw new ValidationError('Expected a number, got NaN')
1310
- }
1311
- if (value - value !== 0) {
1312
- throw new ValidationError(`Expected a finite number, got ${value}`)
1313
- }
1314
- throw new ValidationError(`Expected an integer, got ${value}`)
1059
+ export const integer = number.check((value) => {
1060
+ if (!Number.isInteger(value)) throw new ValidationError(`Expected an integer, got ${value}`)
1315
1061
  })
1316
1062
  /**
1317
1063
  * Validator that ensures a value is a non-negative integer (\>= 0).
@@ -1326,23 +1072,8 @@ export const integer = new Validator<number>((value) => {
1326
1072
  * ```
1327
1073
  * @public
1328
1074
  */
1329
- export const positiveInteger = new Validator<number>((value) => {
1330
- if (Number.isInteger(value) && (value as number) >= 0) {
1331
- return value as number
1332
- }
1333
- if (typeof value !== 'number') {
1334
- throw new ValidationError(`Expected number, got ${typeToString(value)}`)
1335
- }
1336
- if (value !== value) {
1337
- throw new ValidationError('Expected a number, got NaN')
1338
- }
1339
- if (value - value !== 0) {
1340
- throw new ValidationError(`Expected a finite number, got ${value}`)
1341
- }
1342
- if (value < 0) {
1343
- throw new ValidationError(`Expected a positive integer, got ${value}`)
1344
- }
1345
- throw new ValidationError(`Expected an integer, got ${value}`)
1075
+ export const positiveInteger = integer.check((value) => {
1076
+ if (value < 0) throw new ValidationError(`Expected a positive integer, got ${value}`)
1346
1077
  })
1347
1078
  /**
1348
1079
  * Validator that ensures a value is a positive integer (\> 0). Rejects zero and negative integers.
@@ -1355,23 +1086,8 @@ export const positiveInteger = new Validator<number>((value) => {
1355
1086
  * ```
1356
1087
  * @public
1357
1088
  */
1358
- export const nonZeroInteger = new Validator<number>((value) => {
1359
- if (Number.isInteger(value) && (value as number) > 0) {
1360
- return value as number
1361
- }
1362
- if (typeof value !== 'number') {
1363
- throw new ValidationError(`Expected number, got ${typeToString(value)}`)
1364
- }
1365
- if (value !== value) {
1366
- throw new ValidationError('Expected a number, got NaN')
1367
- }
1368
- if (value - value !== 0) {
1369
- throw new ValidationError(`Expected a finite number, got ${value}`)
1370
- }
1371
- if (value <= 0) {
1372
- throw new ValidationError(`Expected a non-zero positive integer, got ${value}`)
1373
- }
1374
- throw new ValidationError(`Expected an integer, got ${value}`)
1089
+ export const nonZeroInteger = integer.check((value) => {
1090
+ if (value <= 0) throw new ValidationError(`Expected a non-zero positive integer, got ${value}`)
1375
1091
  })
1376
1092
 
1377
1093
  /**
@@ -1820,14 +1536,13 @@ export function optional<T>(validator: Validatable<T>): Validator<T | undefined>
1820
1536
  return validator.validate(value)
1821
1537
  },
1822
1538
  (knownGoodValue, newValue) => {
1539
+ if (knownGoodValue === undefined && newValue === undefined) return undefined
1823
1540
  if (newValue === undefined) return undefined
1824
1541
  if (validator.validateUsingKnownGoodVersion && knownGoodValue !== undefined) {
1825
1542
  return validator.validateUsingKnownGoodVersion(knownGoodValue as T, newValue)
1826
1543
  }
1827
1544
  return validator.validate(newValue)
1828
- },
1829
- // Propagate skipSameValueCheck from inner validator to allow refine wrappers
1830
- validator instanceof Validator && validator.skipSameValueCheck
1545
+ }
1831
1546
  )
1832
1547
  }
1833
1548
 
@@ -1857,9 +1572,7 @@ export function nullable<T>(validator: Validatable<T>): Validator<T | null> {
1857
1572
  return validator.validateUsingKnownGoodVersion(knownGoodValue as T, newValue)
1858
1573
  }
1859
1574
  return validator.validate(newValue)
1860
- },
1861
- // Propagate skipSameValueCheck from inner validator to allow refine wrappers
1862
- validator instanceof Validator && validator.skipSameValueCheck
1575
+ }
1863
1576
  )
1864
1577
  }
1865
1578
 
@@ -117,56 +117,23 @@ describe('validations', () => {
117
117
  `[ValidationError: At animal(type = cat).meow: Expected boolean, got undefined]`
118
118
  )
119
119
  })
120
-
121
- it('Rejects Infinity and -Infinity in numberUnion discriminators', () => {
122
- const numberUnionSchema = T.numberUnion('version', {
123
- 1: T.object({ version: T.literal(1), data: T.string }),
124
- 2: T.object({ version: T.literal(2), data: T.string }),
125
- })
126
-
127
- // Valid cases
128
- expect(numberUnionSchema.validate({ version: 1, data: 'hello' })).toEqual({
129
- version: 1,
130
- data: 'hello',
131
- })
132
- expect(numberUnionSchema.validate({ version: 2, data: 'world' })).toEqual({
133
- version: 2,
134
- data: 'world',
135
- })
136
-
137
- // Should reject Infinity
138
- expect(() =>
139
- numberUnionSchema.validate({ version: Infinity, data: 'test' })
140
- ).toThrowErrorMatchingInlineSnapshot(
141
- `[ValidationError: At null: Expected a number for key "version", got "Infinity"]`
142
- )
143
-
144
- // Should reject -Infinity
145
- expect(() =>
146
- numberUnionSchema.validate({ version: -Infinity, data: 'test' })
147
- ).toThrowErrorMatchingInlineSnapshot(
148
- `[ValidationError: At null: Expected a number for key "version", got "-Infinity"]`
149
- )
150
-
151
- // Should reject NaN
152
- expect(() => numberUnionSchema.validate({ version: NaN, data: 'test' })).toThrowError(
153
- /Expected a number for key "version"/
154
- )
155
- })
156
120
  })
157
121
 
158
122
  describe('T.refine', () => {
159
123
  it('Refines a validator.', () => {
160
- // refine can transform values (e.g., string to number)
161
124
  const stringToNumber = T.string.refine((str) => parseInt(str, 10))
125
+ const originalEnv = process.env.NODE_ENV
126
+ process.env.NODE_ENV = 'production'
162
127
  expect(stringToNumber.validate('42')).toBe(42)
128
+ process.env.NODE_ENV = originalEnv
163
129
 
164
- // refine can also modify values of the same type
165
130
  const prefixedString = T.string.refine((str) =>
166
131
  str.startsWith('prefix:') ? str : `prefix:${str}`
167
132
  )
133
+ process.env.NODE_ENV = 'production'
168
134
  expect(prefixedString.validate('test')).toBe('prefix:test')
169
135
  expect(prefixedString.validate('prefix:existing')).toBe('prefix:existing')
136
+ process.env.NODE_ENV = originalEnv
170
137
  })
171
138
 
172
139
  it('Produces a type error if the refinement is not of the correct type.', () => {