@tldraw/validate 5.2.0-next.5d769d321393 → 5.2.0-next.a6104dd18d03

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/DOCS.md ADDED
@@ -0,0 +1,888 @@
1
+ # @tldraw/validate Documentation
2
+
3
+ ## 1. Introduction
4
+
5
+ ### What is @tldraw/validate?
6
+
7
+ @tldraw/validate is a TypeScript library for runtime validation that combines type safety with performance optimization. It provides validators for data structures ranging from simple primitives to complex nested objects, with detailed error reporting and composable validation logic.
8
+
9
+ This library provides the validation foundation for the tldraw SDK, ensuring type safety and data integrity across shape definitions, store operations, and external data handling.
10
+
11
+ ### Installation
12
+
13
+ ```bash
14
+ npm install @tldraw/validate
15
+ ```
16
+
17
+ ### TypeScript
18
+
19
+ @tldraw/validate is written in TypeScript and provides type safety out of the box. All validators maintain full type information, ensuring your validated data has the correct types at both compile time and runtime.
20
+
21
+ ### Quick Example
22
+
23
+ Here's a simple example showing the core validation concepts:
24
+
25
+ ```ts
26
+ import { T } from '@tldraw/validate'
27
+
28
+ // Create some validators
29
+ const userValidator = T.object({
30
+ name: T.string,
31
+ age: T.positiveInteger,
32
+ email: T.linkUrl.optional(),
33
+ })
34
+
35
+ // Validate data safely
36
+ try {
37
+ const user = userValidator.validate({
38
+ name: 'Alice',
39
+ age: 25,
40
+ email: 'alice@example.com',
41
+ })
42
+ // user is now typed as { name: string; age: number; email?: string }
43
+ console.log(`Hello ${user.name}!`)
44
+ } catch (error) {
45
+ console.error('Validation failed:', error.message)
46
+ }
47
+ ```
48
+
49
+ In just a few lines, you've created type-safe validation that catches errors at runtime and provides detailed feedback when validation fails.
50
+
51
+ ## 2. Core Concepts
52
+
53
+ ### The Validator Class
54
+
55
+ A **Validator** is the fundamental building block of the validation system. It's a container that holds validation logic and provides methods for safely checking and converting unknown data into typed values.
56
+
57
+ ```ts
58
+ import { T } from '@tldraw/validate'
59
+
60
+ const numberValidator = T.number
61
+ const result = numberValidator.validate(42) // Returns 42 as number
62
+ ```
63
+
64
+ Every validator implements the same core interface, allowing them to be composed and chained together in powerful ways.
65
+
66
+ #### Basic Validation Methods
67
+
68
+ All validators provide these essential methods:
69
+
70
+ **validate(value)** - Validates a value and returns it with correct typing, or throws an error:
71
+
72
+ ```ts
73
+ const validated = T.string.validate('hello') // Returns "hello" as string
74
+ // T.string.validate(123) // Throws ValidationError
75
+ ```
76
+
77
+ **isValid(value)** - Checks if a value is valid without throwing:
78
+
79
+ ```ts
80
+ if (T.number.isValid(someValue)) {
81
+ // someValue is now typed as number within this block
82
+ console.log(someValue + 1)
83
+ }
84
+ ```
85
+
86
+ **validateUsingKnownGoodVersion(knownGood, newValue)** - Performance-optimized validation:
87
+
88
+ ```ts
89
+ // If newValue hasn't changed, returns knownGood without re-validation
90
+ const optimized = validator.validateUsingKnownGoodVersion(previousValue, newValue)
91
+ ```
92
+
93
+ > Tip: `validateUsingKnownGoodVersion` is a powerful performance optimization that avoids re-validating unchanged data, especially useful for large objects and frequent validation calls.
94
+
95
+ ### Validation Errors
96
+
97
+ When validation fails, @tldraw/validate provides detailed error information through the **ValidationError** class:
98
+
99
+ ```ts
100
+ try {
101
+ T.positiveInteger.validate(-5)
102
+ } catch (error) {
103
+ console.log(error.message) // "Expected a positive integer, got -5"
104
+ console.log(error.path) // Array showing location in nested structures
105
+ }
106
+ ```
107
+
108
+ For complex nested structures, errors include precise path information:
109
+
110
+ ```ts
111
+ const complexValidator = T.object({
112
+ users: T.arrayOf(
113
+ T.object({
114
+ name: T.string,
115
+ settings: T.object({
116
+ theme: T.literalEnum('light', 'dark'),
117
+ }),
118
+ })
119
+ ),
120
+ })
121
+
122
+ // Error: "At users.0.settings.theme: Expected 'light' or 'dark', got 'blue'"
123
+ ```
124
+
125
+ ## 3. Primitive Validators
126
+
127
+ ### Basic Types
128
+
129
+ @tldraw/validate provides validators for all TypeScript primitive types:
130
+
131
+ ```ts
132
+ import { T } from '@tldraw/validate'
133
+
134
+ // Core types
135
+ const name = T.string.validate('Alice')
136
+ const count = T.number.validate(42)
137
+ const isActive = T.boolean.validate(true)
138
+ const largeNumber = T.bigint.validate(123n)
139
+
140
+ // Special types
141
+ const anything = T.unknown.validate({ any: 'value' })
142
+ const untyped = T.any.validate(someValue) // Escape hatch for any type
143
+ ```
144
+
145
+ **Arrays** can be validated with or without content validation:
146
+
147
+ ```ts
148
+ // Any array
149
+ const items = T.array.validate([1, 'hello', true])
150
+
151
+ // Array with validated contents
152
+ const numbers = T.arrayOf(T.number).validate([1, 2, 3])
153
+ ```
154
+
155
+ ### Number Validators
156
+
157
+ Numbers have specialized validators for common validation needs:
158
+
159
+ ```ts
160
+ // Basic number validation (finite, non-NaN)
161
+ const temperature = T.number.validate(23.5)
162
+
163
+ // Non-negative numbers (>= 0)
164
+ const price = T.positiveNumber.validate(29.99)
165
+
166
+ // Positive numbers (> 0)
167
+ const quantity = T.nonZeroNumber.validate(0.01)
168
+
169
+ // Integers
170
+ const wholeNumber = T.integer.validate(42)
171
+
172
+ // Non-negative integers (>= 0)
173
+ const positiveCount = T.positiveInteger.validate(5)
174
+
175
+ // Positive integers (> 0)
176
+ const itemCount = T.nonZeroInteger.validate(1)
177
+ ```
178
+
179
+ > Note: `positiveNumber` and `positiveInteger` validate for non-negative values (`>= 0`), while `nonZeroNumber` and `nonZeroInteger` validate for positive values (`> 0`).
180
+
181
+ These validators catch common data issues:
182
+
183
+ ```ts
184
+ T.number.validate(NaN) // Throws: "Expected a finite number, got NaN"
185
+ T.positiveNumber.validate(-1) // Throws: "Expected a positive number, got -1"
186
+ T.nonZeroNumber.validate(0) // Throws: "Expected a non-zero positive number, got 0"
187
+ T.integer.validate(3.14) // Throws: "Expected an integer, got 3.14"
188
+ ```
189
+
190
+ ### URL Validators
191
+
192
+ URLs require careful validation for security. @tldraw/validate provides context-specific URL validators:
193
+
194
+ ```ts
195
+ // Safe for user-facing links (http, https, mailto)
196
+ const blogLink = T.linkUrl.validate('https://example.com')
197
+ const contactEmail = T.linkUrl.validate('mailto:hello@example.com')
198
+
199
+ // Safe for resource loading (http, https, data URLs, asset URLs)
200
+ const imageSource = T.srcUrl.validate('data:image/png;base64,...')
201
+ const staticAsset = T.srcUrl.validate('https://cdn.example.com/image.jpg')
202
+
203
+ // Strict HTTP/HTTPS only
204
+ const apiEndpoint = T.httpUrl.validate('https://api.example.com')
205
+ ```
206
+
207
+ > Tip: Always use the most restrictive URL validator for your use case. `linkUrl` prevents XSS in user-generated links, while `srcUrl` allows data URLs for embedded content.
208
+
209
+ ### Literal and Enum Validators
210
+
211
+ For validating specific values or sets of values:
212
+
213
+ ```ts
214
+ // Single literal value
215
+ const appName = T.literal('tldraw').validate('tldraw')
216
+
217
+ // Multiple allowed values
218
+ const theme = T.literalEnum('light', 'dark', 'auto').validate('dark')
219
+
220
+ // Using a Set for enum values
221
+ const allowedMethods = new Set(['GET', 'POST', 'PUT'] as const)
222
+ const method = T.setEnum(allowedMethods).validate('GET')
223
+ ```
224
+
225
+ ## 4. Complex Validators
226
+
227
+ ### Object Validation
228
+
229
+ The **object validator** handles structured data with type-safe property validation:
230
+
231
+ ```ts
232
+ // Define the shape of your data
233
+ const userValidator = T.object({
234
+ id: T.string,
235
+ name: T.string,
236
+ age: T.positiveInteger,
237
+ isAdmin: T.boolean,
238
+ lastLogin: T.string.optional(),
239
+ })
240
+
241
+ // Validate objects
242
+ const user = userValidator.validate({
243
+ id: 'user123',
244
+ name: 'Bob',
245
+ age: 30,
246
+ isAdmin: false,
247
+ // lastLogin is optional, can be omitted
248
+ })
249
+ // user is typed as: { id: string; name: string; age: number; isAdmin: boolean; lastLogin?: string }
250
+ ```
251
+
252
+ #### Object Options
253
+
254
+ Objects are strict by default but can be configured:
255
+
256
+ ```ts
257
+ // Allow extra properties (they'll be preserved but not typed)
258
+ const flexibleUser = T.object({
259
+ name: T.string,
260
+ age: T.number,
261
+ }).allowUnknownProperties()
262
+
263
+ flexibleUser.validate({
264
+ name: 'Alice',
265
+ age: 25,
266
+ favoriteColor: 'blue', // This won't cause an error
267
+ })
268
+ ```
269
+
270
+ #### Extending Objects
271
+
272
+ Create new validators by extending existing ones:
273
+
274
+ ```ts
275
+ const baseUser = T.object({
276
+ name: T.string,
277
+ age: T.number,
278
+ })
279
+
280
+ const adminUser = baseUser.extend({
281
+ permissions: T.arrayOf(T.string),
282
+ lastLogin: T.string,
283
+ })
284
+ // adminUser validates: { name: string; age: number; permissions: string[]; lastLogin: string }
285
+ ```
286
+
287
+ ### Array Validation
288
+
289
+ Arrays can be validated with content constraints:
290
+
291
+ ```ts
292
+ // Array of specific type
293
+ const numbers = T.arrayOf(T.number)
294
+ const validNumbers = numbers.validate([1, 2, 3.14, -5])
295
+
296
+ // Array with constraints
297
+ const nonEmptyNames = T.arrayOf(T.string).nonEmpty()
298
+ const atLeastTwo = T.arrayOf(T.string).lengthGreaterThan1()
299
+
300
+ // Nested arrays
301
+ const matrix = T.arrayOf(T.arrayOf(T.number))
302
+ const grid = matrix.validate([
303
+ [1, 2],
304
+ [3, 4],
305
+ ])
306
+ ```
307
+
308
+ ### Dictionary Validation
309
+
310
+ For objects used as key-value maps:
311
+
312
+ ```ts
313
+ // String keys to number values
314
+ const scores = T.dict(T.string, T.number)
315
+ const gameScores = scores.validate({
316
+ alice: 100,
317
+ bob: 85,
318
+ charlie: 92,
319
+ })
320
+
321
+ // Complex value types
322
+ const userPreferences = T.dict(
323
+ T.string,
324
+ T.object({
325
+ theme: T.literalEnum('light', 'dark'),
326
+ notifications: T.boolean,
327
+ })
328
+ )
329
+ ```
330
+
331
+ ### Union Validation
332
+
333
+ For discriminated unions (objects that can be one of several types):
334
+
335
+ ```ts
336
+ // Shape definitions with discriminated unions
337
+ const shapeValidator = T.union('type', {
338
+ rectangle: T.object({
339
+ type: T.literal('rectangle'),
340
+ width: T.positiveNumber,
341
+ height: T.positiveNumber,
342
+ }),
343
+ circle: T.object({
344
+ type: T.literal('circle'),
345
+ radius: T.positiveNumber,
346
+ }),
347
+ triangle: T.object({
348
+ type: T.literal('triangle'),
349
+ base: T.positiveNumber,
350
+ height: T.positiveNumber,
351
+ }),
352
+ })
353
+
354
+ // Validates based on the discriminator field
355
+ const rectangle = shapeValidator.validate({
356
+ type: 'rectangle',
357
+ width: 100,
358
+ height: 50,
359
+ })
360
+ // rectangle is typed as: { type: 'rectangle'; width: number; height: number }
361
+ ```
362
+
363
+ ## 5. Composing Validators
364
+
365
+ ### Nullable and Optional Values
366
+
367
+ Transform validators to accept null or undefined:
368
+
369
+ ```ts
370
+ const optionalString = T.string.optional() // string | undefined
371
+ const nullableNumber = T.number.nullable() // number | null
372
+
373
+ // Can also use helper functions
374
+ const maybeUser = T.optional(userValidator) // User | undefined
375
+ const userOrNull = T.nullable(userValidator) // User | null
376
+ ```
377
+
378
+ ### Refinement and Checks
379
+
380
+ Add additional validation logic to existing validators:
381
+
382
+ #### Using refine() for Transformations
383
+
384
+ **refine()** validates and potentially transforms the value:
385
+
386
+ ```ts
387
+ // Transform string to uppercase
388
+ const upperCaseString = T.string.refine((value) => {
389
+ return value.toUpperCase()
390
+ })
391
+
392
+ const result = upperCaseString.validate('hello') // Returns "HELLO"
393
+
394
+ // Validate and parse JSON
395
+ const jsonValidator = T.string.refine((str) => {
396
+ try {
397
+ return JSON.parse(str)
398
+ } catch (error) {
399
+ throw new Error('Invalid JSON')
400
+ }
401
+ })
402
+ ```
403
+
404
+ #### Using check() for Additional Constraints
405
+
406
+ **check()** adds validation without changing the value. It has two forms:
407
+
408
+ `check(checkFn)`:
409
+
410
+ ```ts
411
+ const evenNumber = T.number.check((value) => {
412
+ if (value % 2 !== 0) {
413
+ throw new Error('Number must be even')
414
+ }
415
+ })
416
+ ```
417
+
418
+ `check(name, checkFn)`:
419
+
420
+ You can also provide a name for the check, which will be included in error messages for easier debugging.
421
+
422
+ ```ts
423
+ const strongPassword = T.string
424
+ .check('min-length', (password) => {
425
+ if (password.length < 8) {
426
+ throw new Error('Password must be at least 8 characters')
427
+ }
428
+ })
429
+ .check('uppercase', (password) => {
430
+ if (!/[A-Z]/.test(password)) {
431
+ throw new Error('Password must contain an uppercase letter')
432
+ }
433
+ })
434
+ .check('number', (password) => {
435
+ if (!/[0-9]/.test(password)) {
436
+ throw new Error('Password must contain a number')
437
+ }
438
+ })
439
+
440
+ // Example error:
441
+ // "At (check uppercase): Password must contain an uppercase letter"
442
+ ```
443
+
444
+ ### Union Types
445
+
446
+ Combine multiple validators with **or()**:
447
+
448
+ ```ts
449
+ const stringOrNumber = T.or(T.string, T.number)
450
+ const value1 = stringOrNumber.validate('hello') // string
451
+ const value2 = stringOrNumber.validate(42) // number
452
+
453
+ // More complex unions
454
+ const idValidator = T.or(
455
+ T.string, // UUID string
456
+ T.positiveInteger // Numeric ID
457
+ )
458
+ ```
459
+
460
+ ## 6. Advanced Features
461
+
462
+ ### Performance Optimization with Known Good Values
463
+
464
+ The validation system includes powerful performance optimizations for repeated validation:
465
+
466
+ ```ts
467
+ // First validation - full validation occurs
468
+ const user = userValidator.validate(userData)
469
+
470
+ // Later validation with mostly unchanged data
471
+ const updatedUser = userValidator.validateUsingKnownGoodVersion(
472
+ user, // Previous valid value
473
+ newUserData // New data to validate
474
+ )
475
+
476
+ // If newUserData is identical to user, returns user immediately
477
+ // If partially changed, only validates the changed parts
478
+ // If completely different, performs full validation
479
+ ```
480
+
481
+ This is particularly powerful for complex objects:
482
+
483
+ ```ts
484
+ const complexObject = T.object({
485
+ metadata: T.object({
486
+ created: T.string,
487
+ updated: T.string,
488
+ }),
489
+ content: T.arrayOf(
490
+ T.object({
491
+ id: T.string,
492
+ text: T.string,
493
+ tags: T.arrayOf(T.string),
494
+ })
495
+ ),
496
+ })
497
+
498
+ // Only validates changed portions of the structure
499
+ const optimized = complexObject.validateUsingKnownGoodVersion(previouslyValidated, incomingData)
500
+ ```
501
+
502
+ ### Model Validation
503
+
504
+ For named entities with enhanced debugging:
505
+
506
+ ```ts
507
+ // Define a model with a name for better error reporting
508
+ const userModel = T.model(
509
+ 'User',
510
+ T.object({
511
+ id: T.string,
512
+ name: T.string,
513
+ email: T.linkUrl,
514
+ })
515
+ )
516
+
517
+ // Errors will include the model name for clarity:
518
+ // "At User.email: Expected a valid URL, got 'invalid-email'"
519
+ ```
520
+
521
+ ### JSON Value Validation
522
+
523
+ Safe handling of JSON data:
524
+
525
+ ```ts
526
+ // Validates any valid JSON value (primitives, arrays, objects)
527
+ const jsonData = T.jsonValue.validate(someUnknownData)
528
+
529
+ // JSON object specifically
530
+ const config = T.jsonDict().validate({
531
+ setting1: 'value1',
532
+ setting2: 42,
533
+ setting3: ['a', 'b', 'c'],
534
+ })
535
+ ```
536
+
537
+ ## 7. Debugging and Error Handling
538
+
539
+ ### Understanding Validation Errors
540
+
541
+ @tldraw/validate provides rich error information to help debug validation issues:
542
+
543
+ ```ts
544
+ const nestedValidator = T.object({
545
+ company: T.object({
546
+ employees: T.arrayOf(
547
+ T.object({
548
+ name: T.string,
549
+ contact: T.object({
550
+ email: T.linkUrl,
551
+ phone: T.string.optional(),
552
+ }),
553
+ })
554
+ ),
555
+ }),
556
+ })
557
+
558
+ try {
559
+ nestedValidator.validate({
560
+ company: {
561
+ employees: [
562
+ {
563
+ name: 'Alice',
564
+ contact: {
565
+ email: 'not-an-email',
566
+ phone: '555-1234',
567
+ },
568
+ },
569
+ ],
570
+ },
571
+ })
572
+ } catch (error) {
573
+ console.log(error.message)
574
+ // "At company.employees.0.contact.email: Expected a valid url, got 'not-an-email'"
575
+
576
+ console.log(error.path)
577
+ // ['company', 'employees', 0, 'contact', 'email']
578
+
579
+ console.log(error.rawMessage)
580
+ // "Expected a valid url, got 'not-an-email'"
581
+ }
582
+ ```
583
+
584
+ ### Error Path Navigation
585
+
586
+ Error paths help you locate exactly where validation failed:
587
+
588
+ - **Property names** appear as strings: `"users"`
589
+ - **Array indices** appear as numbers: `0`, `1`, `2`
590
+ - **Union discriminators** appear with context: `"(type = rectangle)"`
591
+
592
+ ```ts
593
+ // Example paths:
594
+ 'name' // Simple property
595
+ 'users.0' // First item in users array
596
+ 'shape.(type = circle).radius' // radius property of circle variant
597
+ 'config.database.connections.0.host' // Deep nesting
598
+ ```
599
+
600
+ ### Common Validation Patterns
601
+
602
+ #### Validating External APIs
603
+
604
+ ```ts
605
+ const apiResponseValidator = T.object({
606
+ status: T.literalEnum('success', 'error'),
607
+ data: T.object({
608
+ users: T.arrayOf(
609
+ T.object({
610
+ id: T.string,
611
+ name: T.string,
612
+ email: T.linkUrl.optional(),
613
+ })
614
+ ),
615
+ }).optional(),
616
+ error: T.string.optional(),
617
+ })
618
+
619
+ try {
620
+ const response = await fetch('/api/users')
621
+ const json = await response.json()
622
+ const validatedResponse = apiResponseValidator.validate(json)
623
+
624
+ if (validatedResponse.status === 'success') {
625
+ // TypeScript knows data is defined here
626
+ console.log(`Found ${validatedResponse.data.users.length} users`)
627
+ }
628
+ } catch (error) {
629
+ console.error('API response validation failed:', error.message)
630
+ }
631
+ ```
632
+
633
+ #### Validating User Input
634
+
635
+ ```ts
636
+ const userInputValidator = T.object({
637
+ title: T.string.check((title) => {
638
+ if (title.length < 3) {
639
+ throw new Error('Title must be at least 3 characters')
640
+ }
641
+ if (title.length > 100) {
642
+ throw new Error('Title cannot exceed 100 characters')
643
+ }
644
+ }),
645
+ category: T.literalEnum('work', 'personal', 'hobby'),
646
+ priority: T.integer.check((priority) => {
647
+ if (priority < 1 || priority > 5) {
648
+ throw new Error('Priority must be between 1 and 5')
649
+ }
650
+ }),
651
+ tags: T.arrayOf(T.string).optional(),
652
+ })
653
+
654
+ // Safe handling of form data
655
+ function processUserInput(formData: unknown) {
656
+ try {
657
+ const validInput = userInputValidator.validate(formData)
658
+ // Process with confidence that data is valid
659
+ return saveToDatabase(validInput)
660
+ } catch (error) {
661
+ // Return user-friendly validation messages
662
+ return { error: error.message }
663
+ }
664
+ }
665
+ ```
666
+
667
+ #### Migration and Schema Validation
668
+
669
+ ```ts
670
+ // Validate data during migrations
671
+ const migrationValidator = T.object({
672
+ version: T.positiveInteger,
673
+ data: T.jsonValue,
674
+ timestamp: T.string,
675
+ })
676
+
677
+ const legacyUserValidator = T.object({
678
+ name: T.string,
679
+ email: T.string, // Old schema: any string
680
+ age: T.number.optional(),
681
+ })
682
+
683
+ const modernUserValidator = T.object({
684
+ name: T.string,
685
+ email: T.linkUrl, // New schema: validated URL
686
+ age: T.positiveInteger.optional(),
687
+ createdAt: T.string,
688
+ })
689
+
690
+ function migrateUser(legacyData: unknown) {
691
+ // Validate old format
692
+ const legacy = legacyUserValidator.validate(legacyData)
693
+
694
+ // Transform to new format with additional validation
695
+ const modern = modernUserValidator.validate({
696
+ name: legacy.name,
697
+ email: legacy.email, // This will now validate as URL
698
+ age: legacy.age,
699
+ createdAt: new Date().toISOString(),
700
+ })
701
+
702
+ return modern
703
+ }
704
+ ```
705
+
706
+ ## 8. Integration with tldraw
707
+
708
+ ### Shape Definition Validation
709
+
710
+ @tldraw/validate is extensively used throughout tldraw for shape validation:
711
+
712
+ ```ts
713
+ // Example from TLImageShape
714
+ const imageShapeProps = T.object({
715
+ w: T.nonZeroNumber,
716
+ h: T.nonZeroNumber,
717
+ playing: T.boolean,
718
+ url: T.linkUrl,
719
+ assetId: T.string.nullable(),
720
+ crop: T.object({
721
+ topLeft: T.object({ x: T.number, y: T.number }),
722
+ bottomRight: T.object({ x: T.number, y: T.number }),
723
+ }).nullable(),
724
+ flipX: T.boolean,
725
+ flipY: T.boolean,
726
+ })
727
+ ```
728
+
729
+ ### Store Integration
730
+
731
+ The validation system integrates with tldraw's reactive store:
732
+
733
+ ```ts
734
+ // Validating records during store operations
735
+ const shapeRecord = shapeValidator.validate(incomingShapeData)
736
+ store.put([shapeRecord])
737
+ ```
738
+
739
+ ### Custom Shape Validation
740
+
741
+ When creating custom shapes, use @tldraw/validate for type safety:
742
+
743
+ ```ts
744
+ // Custom shape with validated properties
745
+ const customShapeValidator = T.object({
746
+ type: T.literal('custom'),
747
+ x: T.number,
748
+ y: T.number,
749
+ customProperty: T.string.check((value) => {
750
+ if (!value.startsWith('custom-')) {
751
+ throw new Error("Custom property must start with 'custom-'")
752
+ }
753
+ }),
754
+ config: T.object({
755
+ color: T.literalEnum('red', 'blue', 'green'),
756
+ size: T.positiveNumber,
757
+ }),
758
+ })
759
+
760
+ export class CustomShapeUtil extends BaseBoxShapeUtil<CustomShape> {
761
+ static override type = 'custom' as const
762
+ static override props = customShapeValidator
763
+ // ... implementation
764
+ }
765
+ ```
766
+
767
+ ## 9. Best Practices
768
+
769
+ ### Validator Organization
770
+
771
+ Structure validators for maintainability:
772
+
773
+ ```ts
774
+ // Group related validators
775
+ const UserValidators = {
776
+ base: T.object({
777
+ id: T.string,
778
+ name: T.string,
779
+ email: T.linkUrl,
780
+ }),
781
+
782
+ create: T.object({
783
+ name: T.string,
784
+ email: T.linkUrl,
785
+ password: T.string.check(validatePasswordStrength),
786
+ }),
787
+
788
+ update: T.object({
789
+ name: T.string.optional(),
790
+ email: T.linkUrl.optional(),
791
+ }),
792
+ }
793
+
794
+ // Compose complex validators from simpler ones
795
+ const PostValidator = T.object({
796
+ title: T.string,
797
+ author: UserValidators.base,
798
+ tags: T.arrayOf(T.string).optional(),
799
+ publishedAt: T.string.nullable(),
800
+ })
801
+ ```
802
+
803
+ ### Error Handling Strategy
804
+
805
+ Design validation with user experience in mind:
806
+
807
+ ```ts
808
+ function validateAndProcess(data: unknown) {
809
+ try {
810
+ const validated = complexValidator.validate(data)
811
+ return { success: true, data: validated }
812
+ } catch (error) {
813
+ if (error instanceof ValidationError) {
814
+ // Convert technical error to user-friendly message
815
+ const userMessage = getUserFriendlyMessage(error.path, error.rawMessage)
816
+ return { success: false, error: userMessage }
817
+ }
818
+ throw error // Re-throw unexpected errors
819
+ }
820
+ }
821
+
822
+ function getUserFriendlyMessage(path: readonly (string | number)[], message: string) {
823
+ const field = path.join('.')
824
+ if (message.includes('Expected a valid url')) {
825
+ return `Please enter a valid URL for ${field}`
826
+ }
827
+ if (message.includes('Expected a positive number')) {
828
+ return `${field} must be a positive number`
829
+ }
830
+ return `Invalid value for ${field}: ${message}`
831
+ }
832
+ ```
833
+
834
+ ### Performance Considerations
835
+
836
+ Use validation efficiently:
837
+
838
+ ```ts
839
+ // Cache validators for reuse
840
+ const userValidatorCache = new Map()
841
+ function getUserValidator(version: string) {
842
+ if (!userValidatorCache.has(version)) {
843
+ userValidatorCache.set(version, createUserValidator(version))
844
+ }
845
+ return userValidatorCache.get(version)
846
+ }
847
+
848
+ // Use known good validation for updates
849
+ let currentUser = userValidator.validate(initialData)
850
+
851
+ function updateUser(changes: unknown) {
852
+ // Merge changes with current user
853
+ const candidate = { ...currentUser, ...changes }
854
+
855
+ // Efficient validation using known good value
856
+ currentUser = userValidator.validateUsingKnownGoodVersion(currentUser, candidate)
857
+ return currentUser
858
+ }
859
+ ```
860
+
861
+ ### Type Safety Patterns
862
+
863
+ Leverage TypeScript integration:
864
+
865
+ ```ts
866
+ // Extract types from validators
867
+ type User = TypeOf<typeof userValidator>
868
+ type CreateUserRequest = TypeOf<typeof createUserValidator>
869
+
870
+ // Use in function signatures
871
+ function processUser(user: User): string {
872
+ // user is fully typed, no runtime checks needed
873
+ return `Processing ${user.name} (${user.email})`
874
+ }
875
+
876
+ // Ensure validator matches interface
877
+ interface ApiResponse {
878
+ data: User[]
879
+ total: number
880
+ }
881
+
882
+ const apiResponseValidator: Validator<ApiResponse> = T.object({
883
+ data: T.arrayOf(userValidator),
884
+ total: T.nonZeroInteger,
885
+ })
886
+ ```
887
+
888
+ @tldraw/validate provides the foundation for type-safe data validation in tldraw applications. Its performance optimizations, detailed error reporting, and seamless TypeScript integration make it essential for handling external data safely within the tldraw SDK ecosystem.
package/README.md CHANGED
@@ -30,9 +30,13 @@ const queryValidator = T.object({
30
30
  queryValidator.validate(request.query)
31
31
  ```
32
32
 
33
+ ## Documentation
34
+
35
+ A `DOCS.md` file is included alongside this README in the published package, with detailed API documentation and usage examples.
36
+
33
37
  ## Contribution
34
38
 
35
- Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
39
+ Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
36
40
 
37
41
  ## License
38
42
 
package/dist-cjs/index.js CHANGED
@@ -41,7 +41,7 @@ var T = __toESM(require("./lib/validation"), 1);
41
41
  var import_validation = require("./lib/validation");
42
42
  (0, import_utils.registerTldrawLibraryVersion)(
43
43
  "@tldraw/validate",
44
- "5.2.0-next.5d769d321393",
44
+ "5.2.0-next.a6104dd18d03",
45
45
  "cjs"
46
46
  );
47
47
  //# sourceMappingURL=index.js.map
@@ -9,7 +9,7 @@ import {
9
9
  } from "./lib/validation.mjs";
10
10
  registerTldrawLibraryVersion(
11
11
  "@tldraw/validate",
12
- "5.2.0-next.5d769d321393",
12
+ "5.2.0-next.a6104dd18d03",
13
13
  "esm"
14
14
  );
15
15
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/validate",
3
3
  "description": "A runtime validation library by tldraw.",
4
- "version": "5.2.0-next.5d769d321393",
4
+ "version": "5.2.0-next.a6104dd18d03",
5
5
  "author": {
6
6
  "name": "tldraw Inc.",
7
7
  "email": "hello@tldraw.com"
@@ -29,7 +29,8 @@
29
29
  "files": [
30
30
  "dist-esm",
31
31
  "dist-cjs",
32
- "src"
32
+ "src",
33
+ "DOCS.md"
33
34
  ],
34
35
  "scripts": {
35
36
  "test-ci": "yarn run -T vitest run --passWithNoTests",
@@ -43,7 +44,7 @@
43
44
  "lint": "yarn run -T tsx ../../internal/scripts/lint.ts"
44
45
  },
45
46
  "dependencies": {
46
- "@tldraw/utils": "5.2.0-next.5d769d321393"
47
+ "@tldraw/utils": "5.2.0-next.a6104dd18d03"
47
48
  },
48
49
  "devDependencies": {
49
50
  "lazyrepo": "0.0.0-alpha.27"
@@ -149,7 +149,7 @@ describe('validations', () => {
149
149
  )
150
150
 
151
151
  // Should reject NaN
152
- expect(() => numberUnionSchema.validate({ version: NaN, data: 'test' })).toThrowError(
152
+ expect(() => numberUnionSchema.validate({ version: NaN, data: 'test' })).toThrow(
153
153
  /Expected a number for key "version"/
154
154
  )
155
155
  })