@tldraw/validate 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b73a0d46b63f

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,7 +9,7 @@ import {
9
9
  } from "./lib/validation.mjs";
10
10
  registerTldrawLibraryVersion(
11
11
  "@tldraw/validate",
12
- "4.1.0-next.b6dfe9bccde9",
12
+ "4.1.0-next.b73a0d46b63f",
13
13
  "esm"
14
14
  );
15
15
  export {
@@ -30,6 +30,12 @@ function formatPath(path) {
30
30
  return formattedPath;
31
31
  }
32
32
  class ValidationError extends Error {
33
+ /**
34
+ * Creates a new ValidationError with contextual information about where the error occurred.
35
+ *
36
+ * rawMessage - The raw error message without path information
37
+ * path - Array indicating the location in the data structure where validation failed
38
+ */
33
39
  constructor(rawMessage, path = []) {
34
40
  const formattedPath = formatPath(path);
35
41
  const indentedMessage = rawMessage.split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
@@ -70,13 +76,35 @@ function typeToString(value) {
70
76
  }
71
77
  }
72
78
  class Validator {
79
+ /**
80
+ * Creates a new Validator instance.
81
+ *
82
+ * validationFn - Function that validates and returns a value of type T
83
+ * validateUsingKnownGoodVersionFn - Optional performance-optimized validation function
84
+ */
73
85
  constructor(validationFn, validateUsingKnownGoodVersionFn) {
74
86
  this.validationFn = validationFn;
75
87
  this.validateUsingKnownGoodVersionFn = validateUsingKnownGoodVersionFn;
76
88
  }
77
89
  /**
78
- * Asserts that the passed value is of the correct type and returns it. The returned value is
90
+ * Validates an unknown value and returns it with the correct type. The returned value is
79
91
  * guaranteed to be referentially equal to the passed value.
92
+ *
93
+ * @param value - The unknown value to validate
94
+ * @returns The validated value with type T
95
+ * @throws ValidationError When validation fails
96
+ * @example
97
+ * ```ts
98
+ * import { T } from '@tldraw/validate'
99
+ *
100
+ * const name = T.string.validate("Alice") // Returns "Alice" as string
101
+ * const title = T.string.validate("") // Returns "" (empty strings are valid)
102
+ *
103
+ * // These will throw ValidationError:
104
+ * T.string.validate(123) // Expected string, got a number
105
+ * T.string.validate(null) // Expected string, got null
106
+ * T.string.validate(undefined) // Expected string, got undefined
107
+ * ```
80
108
  */
81
109
  validate(value) {
82
110
  const validated = this.validationFn(value);
@@ -85,6 +113,31 @@ class Validator {
85
113
  }
86
114
  return validated;
87
115
  }
116
+ /**
117
+ * Performance-optimized validation using a previously validated value. If the new value
118
+ * is referentially equal to the known good value, returns the known good value immediately.
119
+ *
120
+ * @param knownGoodValue - A previously validated value
121
+ * @param newValue - The new value to validate
122
+ * @returns The validated value, potentially reusing the known good value
123
+ * @throws ValidationError When validation fails
124
+ * @example
125
+ * ```ts
126
+ * import { T } from '@tldraw/validate'
127
+ *
128
+ * const userValidator = T.object({
129
+ * name: T.string,
130
+ * settings: T.object({ theme: T.literalEnum('light', 'dark') })
131
+ * })
132
+ *
133
+ * const user = userValidator.validate({ name: "Alice", settings: { theme: "light" } })
134
+ *
135
+ * // Later, with partially changed data:
136
+ * const newData = { name: "Alice", settings: { theme: "dark" } }
137
+ * const updated = userValidator.validateUsingKnownGoodVersion(user, newData)
138
+ * // Only validates the changed 'theme' field for better performance
139
+ * ```
140
+ */
88
141
  validateUsingKnownGoodVersion(knownGoodValue, newValue) {
89
142
  if (Object.is(knownGoodValue, newValue)) {
90
143
  return knownGoodValue;
@@ -94,7 +147,28 @@ class Validator {
94
147
  }
95
148
  return this.validate(newValue);
96
149
  }
97
- /** Checks that the passed value is of the correct type. */
150
+ /**
151
+ * Type guard that checks if a value is valid without throwing an error.
152
+ *
153
+ * @param value - The value to check
154
+ * @returns True if the value is valid, false otherwise
155
+ * @example
156
+ * ```ts
157
+ * import { T } from '@tldraw/validate'
158
+ *
159
+ * function processUserInput(input: unknown) {
160
+ * if (T.string.isValid(input)) {
161
+ * // input is now typed as string within this block
162
+ * return input.toUpperCase()
163
+ * }
164
+ * if (T.number.isValid(input)) {
165
+ * // input is now typed as number within this block
166
+ * return input.toFixed(2)
167
+ * }
168
+ * throw new Error('Expected string or number')
169
+ * }
170
+ * ```
171
+ */
98
172
  isValid(value) {
99
173
  try {
100
174
  this.validate(value);
@@ -104,22 +178,85 @@ class Validator {
104
178
  }
105
179
  }
106
180
  /**
107
- * Returns a new validator that also accepts null or undefined. The resulting value will always be
108
- * null.
181
+ * Returns a new validator that also accepts null values.
182
+ *
183
+ * @returns A new validator that accepts T or null
184
+ * @example
185
+ * ```ts
186
+ * import { T } from '@tldraw/validate'
187
+ *
188
+ * const assetValidator = T.object({
189
+ * id: T.string,
190
+ * name: T.string,
191
+ * src: T.srcUrl.nullable(), // Can be null if not loaded yet
192
+ * mimeType: T.string.nullable()
193
+ * })
194
+ *
195
+ * const asset = assetValidator.validate({
196
+ * id: "image-123",
197
+ * name: "photo.jpg",
198
+ * src: null, // Valid - asset not loaded yet
199
+ * mimeType: "image/jpeg"
200
+ * })
201
+ * ```
109
202
  */
110
203
  nullable() {
111
204
  return nullable(this);
112
205
  }
113
206
  /**
114
- * Returns a new validator that also accepts null or undefined. The resulting value will always be
115
- * null.
207
+ * Returns a new validator that also accepts undefined values.
208
+ *
209
+ * @returns A new validator that accepts T or undefined
210
+ * @example
211
+ * ```ts
212
+ * import { T } from '@tldraw/validate'
213
+ *
214
+ * const shapeConfigValidator = T.object({
215
+ * type: T.literal('rectangle'),
216
+ * x: T.number,
217
+ * y: T.number,
218
+ * label: T.string.optional(), // Optional property
219
+ * metadata: T.object({ created: T.string }).optional()
220
+ * })
221
+ *
222
+ * // Both of these are valid:
223
+ * const shape1 = shapeConfigValidator.validate({ type: 'rectangle', x: 0, y: 0 })
224
+ * const shape2 = shapeConfigValidator.validate({
225
+ * type: 'rectangle', x: 0, y: 0, label: "My Shape"
226
+ * })
227
+ * ```
116
228
  */
117
229
  optional() {
118
230
  return optional(this);
119
231
  }
120
232
  /**
121
- * Refine this validation to a new type. The passed-in validation function should throw an error
122
- * if the value can't be converted to the new type, or return the new type otherwise.
233
+ * Creates a new validator by refining this validator with additional logic that can transform
234
+ * the validated value to a new type.
235
+ *
236
+ * @param otherValidationFn - Function that transforms/validates the value to type U
237
+ * @returns A new validator that validates to type U
238
+ * @throws ValidationError When validation or refinement fails
239
+ * @example
240
+ * ```ts
241
+ * import { T, ValidationError } from '@tldraw/validate'
242
+ *
243
+ * // Transform string to ensure it starts with a prefix
244
+ * const prefixedIdValidator = T.string.refine((id) => {
245
+ * return id.startsWith('shape:') ? id : `shape:${id}`
246
+ * })
247
+ *
248
+ * const id1 = prefixedIdValidator.validate("rectangle-123") // Returns "shape:rectangle-123"
249
+ * const id2 = prefixedIdValidator.validate("shape:circle-456") // Returns "shape:circle-456"
250
+ *
251
+ * // Parse and validate JSON strings
252
+ * const jsonValidator = T.string.refine((str) => {
253
+ * try {
254
+ * return JSON.parse(str)
255
+ * } catch {
256
+ * throw new ValidationError('Invalid JSON string')
257
+ * }
258
+ * })
259
+ * ```
123
260
  */
124
261
  refine(otherValidationFn) {
125
262
  return new Validator(
@@ -150,6 +287,11 @@ class Validator {
150
287
  }
151
288
  }
152
289
  class ArrayOfValidator extends Validator {
290
+ /**
291
+ * Creates a new ArrayOfValidator.
292
+ *
293
+ * itemValidator - Validator used to validate each array element
294
+ */
153
295
  constructor(itemValidator) {
154
296
  super(
155
297
  (value) => {
@@ -186,6 +328,18 @@ class ArrayOfValidator extends Validator {
186
328
  );
187
329
  this.itemValidator = itemValidator;
188
330
  }
331
+ /**
332
+ * Returns a new validator that ensures the array is not empty.
333
+ *
334
+ * @returns A new validator that rejects empty arrays
335
+ * @throws ValidationError When the array is empty
336
+ * @example
337
+ * ```ts
338
+ * const nonEmptyStrings = T.arrayOf(T.string).nonEmpty()
339
+ * nonEmptyStrings.validate(["hello"]) // Valid
340
+ * nonEmptyStrings.validate([]) // Throws ValidationError
341
+ * ```
342
+ */
189
343
  nonEmpty() {
190
344
  return this.check((value) => {
191
345
  if (value.length === 0) {
@@ -193,6 +347,18 @@ class ArrayOfValidator extends Validator {
193
347
  }
194
348
  });
195
349
  }
350
+ /**
351
+ * Returns a new validator that ensures the array has more than one element.
352
+ *
353
+ * @returns A new validator that requires at least 2 elements
354
+ * @throws ValidationError When the array has 1 or fewer elements
355
+ * @example
356
+ * ```ts
357
+ * const multipleItems = T.arrayOf(T.string).lengthGreaterThan1()
358
+ * multipleItems.validate(["a", "b"]) // Valid
359
+ * multipleItems.validate(["a"]) // Throws ValidationError
360
+ * ```
361
+ */
196
362
  lengthGreaterThan1() {
197
363
  return this.check((value) => {
198
364
  if (value.length <= 1) {
@@ -202,6 +368,12 @@ class ArrayOfValidator extends Validator {
202
368
  }
203
369
  }
204
370
  class ObjectValidator extends Validator {
371
+ /**
372
+ * Creates a new ObjectValidator.
373
+ *
374
+ * config - Object mapping property names to their validators
375
+ * shouldAllowUnknownProperties - Whether to allow properties not defined in config
376
+ */
205
377
  constructor(config, shouldAllowUnknownProperties = false) {
206
378
  super(
207
379
  (object2) => {
@@ -265,21 +437,32 @@ class ObjectValidator extends Validator {
265
437
  this.config = config;
266
438
  this.shouldAllowUnknownProperties = shouldAllowUnknownProperties;
267
439
  }
440
+ /**
441
+ * Returns a new validator that allows unknown properties in the validated object.
442
+ *
443
+ * @returns A new ObjectValidator that accepts extra properties
444
+ * @example
445
+ * ```ts
446
+ * const flexibleUser = T.object({ name: T.string }).allowUnknownProperties()
447
+ * flexibleUser.validate({ name: "Alice", extra: "allowed" }) // Valid
448
+ * ```
449
+ */
268
450
  allowUnknownProperties() {
269
451
  return new ObjectValidator(this.config, true);
270
452
  }
271
453
  /**
272
- * Extend an object validator by adding additional properties.
454
+ * Creates a new ObjectValidator by extending this validator with additional properties.
273
455
  *
456
+ * @param extension - Object mapping new property names to their validators
457
+ * @returns A new ObjectValidator that validates both original and extended properties
274
458
  * @example
275
- *
276
459
  * ```ts
277
- * const animalValidator = T.object({
278
- * name: T.string,
279
- * })
280
- * const catValidator = animalValidator.extend({
281
- * meowVolume: T.number,
460
+ * const baseUser = T.object({ name: T.string, age: T.number })
461
+ * const adminUser = baseUser.extend({
462
+ * permissions: T.arrayOf(T.string),
463
+ * isAdmin: T.boolean
282
464
  * })
465
+ * // adminUser validates: { name: string; age: number; permissions: string[]; isAdmin: boolean }
283
466
  * ```
284
467
  */
285
468
  extend(extension) {
@@ -287,6 +470,14 @@ class ObjectValidator extends Validator {
287
470
  }
288
471
  }
289
472
  class UnionValidator extends Validator {
473
+ /**
474
+ * Creates a new UnionValidator.
475
+ *
476
+ * key - The discriminator property name used to determine the variant
477
+ * config - Object mapping variant names to their validators
478
+ * unknownValueValidation - Function to handle unknown variants
479
+ * useNumberKeys - Whether the discriminator uses number keys instead of strings
480
+ */
290
481
  constructor(key, config, unknownValueValidation, useNumberKeys) {
291
482
  super(
292
483
  (input) => {
@@ -338,11 +529,31 @@ class UnionValidator extends Validator {
338
529
  const matchingSchema = hasOwnProperty(this.config, variant) ? this.config[variant] : void 0;
339
530
  return { matchingSchema, variant };
340
531
  }
532
+ /**
533
+ * Returns a new UnionValidator that can handle unknown variants using the provided function.
534
+ *
535
+ * @param unknownValueValidation - Function to validate/transform unknown variants
536
+ * @returns A new UnionValidator that accepts unknown variants
537
+ * @example
538
+ * ```ts
539
+ * const shapeValidator = T.union('type', { circle: circleValidator })
540
+ * .validateUnknownVariants((obj, variant) => {
541
+ * console.warn(`Unknown shape type: ${variant}`)
542
+ * return obj as UnknownShape
543
+ * })
544
+ * ```
545
+ */
341
546
  validateUnknownVariants(unknownValueValidation) {
342
547
  return new UnionValidator(this.key, this.config, unknownValueValidation, this.useNumberKeys);
343
548
  }
344
549
  }
345
550
  class DictValidator extends Validator {
551
+ /**
552
+ * Creates a new DictValidator.
553
+ *
554
+ * keyValidator - Validator for object keys
555
+ * valueValidator - Validator for object values
556
+ */
346
557
  constructor(keyValidator, valueValidator) {
347
558
  super(
348
559
  (object2) => {