@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 +888 -0
- package/README.md +5 -1
- package/dist-cjs/index.js +1 -1
- package/dist-esm/index.mjs +1 -1
- package/package.json +4 -3
- package/src/test/validation.test.ts +1 -1
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
|
-
|
|
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.
|
|
44
|
+
"5.2.0-next.a6104dd18d03",
|
|
45
45
|
"cjs"
|
|
46
46
|
);
|
|
47
47
|
//# sourceMappingURL=index.js.map
|
package/dist-esm/index.mjs
CHANGED
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.
|
|
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.
|
|
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' })).
|
|
152
|
+
expect(() => numberUnionSchema.validate({ version: NaN, data: 'test' })).toThrow(
|
|
153
153
|
/Expected a number for key "version"/
|
|
154
154
|
)
|
|
155
155
|
})
|