@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.
- package/dist-cjs/index.d.ts +771 -77
- package/dist-cjs/index.js +2 -2
- package/dist-cjs/lib/validation.js +226 -15
- package/dist-cjs/lib/validation.js.map +2 -2
- package/dist-esm/index.d.mts +771 -77
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/validation.mjs +226 -15
- package/dist-esm/lib/validation.mjs.map +2 -2
- package/package.json +2 -2
- package/src/lib/validation.ts +778 -78
- package/src/test/validation.test.ts +54 -3
package/src/lib/validation.ts
CHANGED
|
@@ -9,25 +9,87 @@ import {
|
|
|
9
9
|
validateIndexKey,
|
|
10
10
|
} from '@tldraw/utils'
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* A function that validates and returns a value of type T from unknown input.
|
|
14
|
+
* The function should throw a ValidationError if the value is invalid.
|
|
15
|
+
*
|
|
16
|
+
* @param value - The unknown value to validate
|
|
17
|
+
* @returns The validated value of type T
|
|
18
|
+
* @throws \{ValidationError\} When the value doesn't match the expected type
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const stringValidator: ValidatorFn<string> = (value) => {
|
|
22
|
+
* if (typeof value !== 'string') {
|
|
23
|
+
* throw new ValidationError('Expected string')
|
|
24
|
+
* }
|
|
25
|
+
* return value
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
13
30
|
export type ValidatorFn<T> = (value: unknown) => T
|
|
14
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* A performance-optimized validation function that can use a previously validated value
|
|
33
|
+
* to avoid revalidating unchanged parts of the data structure.
|
|
34
|
+
*
|
|
35
|
+
* @param knownGoodValue - A previously validated value of type In
|
|
36
|
+
* @param value - The unknown value to validate
|
|
37
|
+
* @returns The validated value of type Out
|
|
38
|
+
* @throws ValidationError When the value doesn't match the expected type
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const optimizedValidator: ValidatorUsingKnownGoodVersionFn<User> = (
|
|
42
|
+
* knownGood,
|
|
43
|
+
* newValue
|
|
44
|
+
* ) => {
|
|
45
|
+
* if (Object.is(knownGood, newValue)) return knownGood
|
|
46
|
+
* return fullValidation(newValue)
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
15
51
|
export type ValidatorUsingKnownGoodVersionFn<In, Out = In> = (
|
|
16
52
|
knownGoodValue: In,
|
|
17
53
|
value: unknown
|
|
18
54
|
) => Out
|
|
19
55
|
|
|
20
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Interface for objects that can validate unknown values and return typed results.
|
|
58
|
+
* This is the core interface implemented by all validators in the validation system.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const customValidator: Validatable<number> = {
|
|
63
|
+
* validate(value) {
|
|
64
|
+
* if (typeof value !== 'number') {
|
|
65
|
+
* throw new ValidationError('Expected number')
|
|
66
|
+
* }
|
|
67
|
+
* return value
|
|
68
|
+
* }
|
|
69
|
+
* }
|
|
70
|
+
* ```
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
21
73
|
export interface Validatable<T> {
|
|
74
|
+
/**
|
|
75
|
+
* Validates an unknown value and returns it with the correct type.
|
|
76
|
+
*
|
|
77
|
+
* @param value - The unknown value to validate
|
|
78
|
+
* @returns The validated value with type T
|
|
79
|
+
* @throws ValidationError When validation fails
|
|
80
|
+
*/
|
|
22
81
|
validate(value: unknown): T
|
|
23
82
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* any part of it has not changed since the last validation.
|
|
83
|
+
* Performance-optimized validation that can use a previously validated value
|
|
84
|
+
* to avoid revalidating unchanged parts of the data structure.
|
|
27
85
|
*
|
|
28
86
|
* If the value has not changed but is not referentially equal, the function
|
|
29
87
|
* should return the previous value.
|
|
30
|
-
*
|
|
88
|
+
*
|
|
89
|
+
* @param knownGoodValue - A previously validated value
|
|
90
|
+
* @param newValue - The new value to validate
|
|
91
|
+
* @returns The validated value, potentially reusing the known good value for performance
|
|
92
|
+
* @throws ValidationError When validation fails
|
|
31
93
|
*/
|
|
32
94
|
validateUsingKnownGoodVersion?(knownGoodValue: T, newValue: unknown): T
|
|
33
95
|
}
|
|
@@ -61,10 +123,33 @@ function formatPath(path: ReadonlyArray<number | string>): string | null {
|
|
|
61
123
|
return formattedPath
|
|
62
124
|
}
|
|
63
125
|
|
|
64
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Error thrown when validation fails. Provides detailed information about what went wrong
|
|
128
|
+
* and where in the data structure the error occurred.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* try {
|
|
133
|
+
* validator.validate(invalidData)
|
|
134
|
+
* } catch (error) {
|
|
135
|
+
* if (error instanceof ValidationError) {
|
|
136
|
+
* console.log(error.message) // "At users.0.email: Expected valid URL"
|
|
137
|
+
* console.log(error.path) // ['users', 0, 'email']
|
|
138
|
+
* console.log(error.rawMessage) // "Expected valid URL"
|
|
139
|
+
* }
|
|
140
|
+
* }
|
|
141
|
+
* ```
|
|
142
|
+
* @public
|
|
143
|
+
*/
|
|
65
144
|
export class ValidationError extends Error {
|
|
66
145
|
override name = 'ValidationError'
|
|
67
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Creates a new ValidationError with contextual information about where the error occurred.
|
|
149
|
+
*
|
|
150
|
+
* rawMessage - The raw error message without path information
|
|
151
|
+
* path - Array indicating the location in the data structure where validation failed
|
|
152
|
+
*/
|
|
68
153
|
constructor(
|
|
69
154
|
public readonly rawMessage: string,
|
|
70
155
|
public readonly path: ReadonlyArray<number | string> = []
|
|
@@ -110,19 +195,67 @@ function typeToString(value: unknown): string {
|
|
|
110
195
|
}
|
|
111
196
|
}
|
|
112
197
|
|
|
113
|
-
/**
|
|
198
|
+
/**
|
|
199
|
+
* Utility type that extracts the validated type from a Validatable object.
|
|
200
|
+
* Useful for deriving TypeScript types from validator definitions.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```ts
|
|
204
|
+
* const userValidator = T.object({ name: T.string, age: T.number })
|
|
205
|
+
* type User = TypeOf<typeof userValidator> // { name: string; age: number }
|
|
206
|
+
* ```
|
|
207
|
+
* @public
|
|
208
|
+
*/
|
|
114
209
|
export type TypeOf<V extends Validatable<any>> = V extends Validatable<infer T> ? T : never
|
|
115
210
|
|
|
116
|
-
/**
|
|
211
|
+
/**
|
|
212
|
+
* The main validator class that implements the Validatable interface. This is the base class
|
|
213
|
+
* for all validators and provides methods for validation, type checking, and composing validators.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* const numberValidator = new Validator((value) => {
|
|
218
|
+
* if (typeof value !== 'number') {
|
|
219
|
+
* throw new ValidationError('Expected number')
|
|
220
|
+
* }
|
|
221
|
+
* return value
|
|
222
|
+
* })
|
|
223
|
+
*
|
|
224
|
+
* const result = numberValidator.validate(42) // Returns 42 as number
|
|
225
|
+
* ```
|
|
226
|
+
* @public
|
|
227
|
+
*/
|
|
117
228
|
export class Validator<T> implements Validatable<T> {
|
|
229
|
+
/**
|
|
230
|
+
* Creates a new Validator instance.
|
|
231
|
+
*
|
|
232
|
+
* validationFn - Function that validates and returns a value of type T
|
|
233
|
+
* validateUsingKnownGoodVersionFn - Optional performance-optimized validation function
|
|
234
|
+
*/
|
|
118
235
|
constructor(
|
|
119
236
|
readonly validationFn: ValidatorFn<T>,
|
|
120
237
|
readonly validateUsingKnownGoodVersionFn?: ValidatorUsingKnownGoodVersionFn<T>
|
|
121
238
|
) {}
|
|
122
239
|
|
|
123
240
|
/**
|
|
124
|
-
*
|
|
241
|
+
* Validates an unknown value and returns it with the correct type. The returned value is
|
|
125
242
|
* guaranteed to be referentially equal to the passed value.
|
|
243
|
+
*
|
|
244
|
+
* @param value - The unknown value to validate
|
|
245
|
+
* @returns The validated value with type T
|
|
246
|
+
* @throws ValidationError When validation fails
|
|
247
|
+
* @example
|
|
248
|
+
* ```ts
|
|
249
|
+
* import { T } from '@tldraw/validate'
|
|
250
|
+
*
|
|
251
|
+
* const name = T.string.validate("Alice") // Returns "Alice" as string
|
|
252
|
+
* const title = T.string.validate("") // Returns "" (empty strings are valid)
|
|
253
|
+
*
|
|
254
|
+
* // These will throw ValidationError:
|
|
255
|
+
* T.string.validate(123) // Expected string, got a number
|
|
256
|
+
* T.string.validate(null) // Expected string, got null
|
|
257
|
+
* T.string.validate(undefined) // Expected string, got undefined
|
|
258
|
+
* ```
|
|
126
259
|
*/
|
|
127
260
|
validate(value: unknown): T {
|
|
128
261
|
const validated = this.validationFn(value)
|
|
@@ -132,6 +265,31 @@ export class Validator<T> implements Validatable<T> {
|
|
|
132
265
|
return validated
|
|
133
266
|
}
|
|
134
267
|
|
|
268
|
+
/**
|
|
269
|
+
* Performance-optimized validation using a previously validated value. If the new value
|
|
270
|
+
* is referentially equal to the known good value, returns the known good value immediately.
|
|
271
|
+
*
|
|
272
|
+
* @param knownGoodValue - A previously validated value
|
|
273
|
+
* @param newValue - The new value to validate
|
|
274
|
+
* @returns The validated value, potentially reusing the known good value
|
|
275
|
+
* @throws ValidationError When validation fails
|
|
276
|
+
* @example
|
|
277
|
+
* ```ts
|
|
278
|
+
* import { T } from '@tldraw/validate'
|
|
279
|
+
*
|
|
280
|
+
* const userValidator = T.object({
|
|
281
|
+
* name: T.string,
|
|
282
|
+
* settings: T.object({ theme: T.literalEnum('light', 'dark') })
|
|
283
|
+
* })
|
|
284
|
+
*
|
|
285
|
+
* const user = userValidator.validate({ name: "Alice", settings: { theme: "light" } })
|
|
286
|
+
*
|
|
287
|
+
* // Later, with partially changed data:
|
|
288
|
+
* const newData = { name: "Alice", settings: { theme: "dark" } }
|
|
289
|
+
* const updated = userValidator.validateUsingKnownGoodVersion(user, newData)
|
|
290
|
+
* // Only validates the changed 'theme' field for better performance
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
135
293
|
validateUsingKnownGoodVersion(knownGoodValue: T, newValue: unknown): T {
|
|
136
294
|
if (Object.is(knownGoodValue, newValue)) {
|
|
137
295
|
return knownGoodValue as T
|
|
@@ -144,7 +302,28 @@ export class Validator<T> implements Validatable<T> {
|
|
|
144
302
|
return this.validate(newValue)
|
|
145
303
|
}
|
|
146
304
|
|
|
147
|
-
/**
|
|
305
|
+
/**
|
|
306
|
+
* Type guard that checks if a value is valid without throwing an error.
|
|
307
|
+
*
|
|
308
|
+
* @param value - The value to check
|
|
309
|
+
* @returns True if the value is valid, false otherwise
|
|
310
|
+
* @example
|
|
311
|
+
* ```ts
|
|
312
|
+
* import { T } from '@tldraw/validate'
|
|
313
|
+
*
|
|
314
|
+
* function processUserInput(input: unknown) {
|
|
315
|
+
* if (T.string.isValid(input)) {
|
|
316
|
+
* // input is now typed as string within this block
|
|
317
|
+
* return input.toUpperCase()
|
|
318
|
+
* }
|
|
319
|
+
* if (T.number.isValid(input)) {
|
|
320
|
+
* // input is now typed as number within this block
|
|
321
|
+
* return input.toFixed(2)
|
|
322
|
+
* }
|
|
323
|
+
* throw new Error('Expected string or number')
|
|
324
|
+
* }
|
|
325
|
+
* ```
|
|
326
|
+
*/
|
|
148
327
|
isValid(value: unknown): value is T {
|
|
149
328
|
try {
|
|
150
329
|
this.validate(value)
|
|
@@ -155,24 +334,87 @@ export class Validator<T> implements Validatable<T> {
|
|
|
155
334
|
}
|
|
156
335
|
|
|
157
336
|
/**
|
|
158
|
-
* Returns a new validator that also accepts null
|
|
159
|
-
*
|
|
337
|
+
* Returns a new validator that also accepts null values.
|
|
338
|
+
*
|
|
339
|
+
* @returns A new validator that accepts T or null
|
|
340
|
+
* @example
|
|
341
|
+
* ```ts
|
|
342
|
+
* import { T } from '@tldraw/validate'
|
|
343
|
+
*
|
|
344
|
+
* const assetValidator = T.object({
|
|
345
|
+
* id: T.string,
|
|
346
|
+
* name: T.string,
|
|
347
|
+
* src: T.srcUrl.nullable(), // Can be null if not loaded yet
|
|
348
|
+
* mimeType: T.string.nullable()
|
|
349
|
+
* })
|
|
350
|
+
*
|
|
351
|
+
* const asset = assetValidator.validate({
|
|
352
|
+
* id: "image-123",
|
|
353
|
+
* name: "photo.jpg",
|
|
354
|
+
* src: null, // Valid - asset not loaded yet
|
|
355
|
+
* mimeType: "image/jpeg"
|
|
356
|
+
* })
|
|
357
|
+
* ```
|
|
160
358
|
*/
|
|
161
359
|
nullable(): Validator<T | null> {
|
|
162
360
|
return nullable(this)
|
|
163
361
|
}
|
|
164
362
|
|
|
165
363
|
/**
|
|
166
|
-
* Returns a new validator that also accepts
|
|
167
|
-
*
|
|
364
|
+
* Returns a new validator that also accepts undefined values.
|
|
365
|
+
*
|
|
366
|
+
* @returns A new validator that accepts T or undefined
|
|
367
|
+
* @example
|
|
368
|
+
* ```ts
|
|
369
|
+
* import { T } from '@tldraw/validate'
|
|
370
|
+
*
|
|
371
|
+
* const shapeConfigValidator = T.object({
|
|
372
|
+
* type: T.literal('rectangle'),
|
|
373
|
+
* x: T.number,
|
|
374
|
+
* y: T.number,
|
|
375
|
+
* label: T.string.optional(), // Optional property
|
|
376
|
+
* metadata: T.object({ created: T.string }).optional()
|
|
377
|
+
* })
|
|
378
|
+
*
|
|
379
|
+
* // Both of these are valid:
|
|
380
|
+
* const shape1 = shapeConfigValidator.validate({ type: 'rectangle', x: 0, y: 0 })
|
|
381
|
+
* const shape2 = shapeConfigValidator.validate({
|
|
382
|
+
* type: 'rectangle', x: 0, y: 0, label: "My Shape"
|
|
383
|
+
* })
|
|
384
|
+
* ```
|
|
168
385
|
*/
|
|
169
386
|
optional(): Validator<T | undefined> {
|
|
170
387
|
return optional(this)
|
|
171
388
|
}
|
|
172
389
|
|
|
173
390
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
391
|
+
* Creates a new validator by refining this validator with additional logic that can transform
|
|
392
|
+
* the validated value to a new type.
|
|
393
|
+
*
|
|
394
|
+
* @param otherValidationFn - Function that transforms/validates the value to type U
|
|
395
|
+
* @returns A new validator that validates to type U
|
|
396
|
+
* @throws ValidationError When validation or refinement fails
|
|
397
|
+
* @example
|
|
398
|
+
* ```ts
|
|
399
|
+
* import { T, ValidationError } from '@tldraw/validate'
|
|
400
|
+
*
|
|
401
|
+
* // Transform string to ensure it starts with a prefix
|
|
402
|
+
* const prefixedIdValidator = T.string.refine((id) => {
|
|
403
|
+
* return id.startsWith('shape:') ? id : `shape:${id}`
|
|
404
|
+
* })
|
|
405
|
+
*
|
|
406
|
+
* const id1 = prefixedIdValidator.validate("rectangle-123") // Returns "shape:rectangle-123"
|
|
407
|
+
* const id2 = prefixedIdValidator.validate("shape:circle-456") // Returns "shape:circle-456"
|
|
408
|
+
*
|
|
409
|
+
* // Parse and validate JSON strings
|
|
410
|
+
* const jsonValidator = T.string.refine((str) => {
|
|
411
|
+
* try {
|
|
412
|
+
* return JSON.parse(str)
|
|
413
|
+
* } catch {
|
|
414
|
+
* throw new ValidationError('Invalid JSON string')
|
|
415
|
+
* }
|
|
416
|
+
* })
|
|
417
|
+
* ```
|
|
176
418
|
*/
|
|
177
419
|
refine<U>(otherValidationFn: (value: T) => U): Validator<U> {
|
|
178
420
|
return new Validator(
|
|
@@ -191,19 +433,49 @@ export class Validator<T> implements Validatable<T> {
|
|
|
191
433
|
}
|
|
192
434
|
|
|
193
435
|
/**
|
|
194
|
-
*
|
|
436
|
+
* Adds an additional validation check without changing the resulting value type.
|
|
437
|
+
* Can be called with just a check function, or with a name for better error messages.
|
|
195
438
|
*
|
|
439
|
+
* @param name - Name for the check (used in error messages)
|
|
440
|
+
* @param checkFn - Function that validates the value (should throw on invalid input)
|
|
441
|
+
* @returns A new validator with the additional check
|
|
442
|
+
* @throws ValidationError When the check fails
|
|
196
443
|
* @example
|
|
197
|
-
*
|
|
198
444
|
* ```ts
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
445
|
+
* import { T, ValidationError } from '@tldraw/validate'
|
|
446
|
+
*
|
|
447
|
+
* // Basic check without name
|
|
448
|
+
* const evenNumber = T.number.check((value) => {
|
|
449
|
+
* if (value % 2 !== 0) {
|
|
450
|
+
* throw new ValidationError('Expected even number')
|
|
451
|
+
* }
|
|
452
|
+
* })
|
|
453
|
+
*
|
|
454
|
+
* // Named checks for better error messages in complex validators
|
|
455
|
+
* const shapePositionValidator = T.object({
|
|
456
|
+
* x: T.number.check('finite', (value) => {
|
|
457
|
+
* if (!Number.isFinite(value)) {
|
|
458
|
+
* throw new ValidationError('Position must be finite')
|
|
459
|
+
* }
|
|
460
|
+
* }),
|
|
461
|
+
* y: T.number.check('within-bounds', (value) => {
|
|
462
|
+
* if (value < -10000 || value > 10000) {
|
|
463
|
+
* throw new ValidationError('Position must be within bounds (-10000 to 10000)')
|
|
464
|
+
* }
|
|
465
|
+
* })
|
|
203
466
|
* })
|
|
467
|
+
*
|
|
468
|
+
* // Error will be: "At x (check finite): Position must be finite"
|
|
204
469
|
* ```
|
|
205
470
|
*/
|
|
206
471
|
check(name: string, checkFn: (value: T) => void): Validator<T>
|
|
472
|
+
/**
|
|
473
|
+
* Adds an additional validation check without changing the resulting value type.
|
|
474
|
+
*
|
|
475
|
+
* @param checkFn - Function that validates the value (should throw on invalid input)
|
|
476
|
+
* @returns A new validator with the additional check
|
|
477
|
+
* @throws ValidationError When the check fails
|
|
478
|
+
*/
|
|
207
479
|
check(checkFn: (value: T) => void): Validator<T>
|
|
208
480
|
check(nameOrCheckFn: string | ((value: T) => void), checkFn?: (value: T) => void): Validator<T> {
|
|
209
481
|
if (typeof nameOrCheckFn === 'string') {
|
|
@@ -220,8 +492,25 @@ export class Validator<T> implements Validatable<T> {
|
|
|
220
492
|
}
|
|
221
493
|
}
|
|
222
494
|
|
|
223
|
-
/**
|
|
495
|
+
/**
|
|
496
|
+
* Validator for arrays where each element is validated using the provided item validator.
|
|
497
|
+
* Extends the base Validator class with array-specific validation methods.
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```ts
|
|
501
|
+
* const stringArray = new ArrayOfValidator(T.string)
|
|
502
|
+
* const numbers = stringArray.validate(["a", "b", "c"]) // Returns string[]
|
|
503
|
+
*
|
|
504
|
+
* const userArray = T.arrayOf(T.object({ name: T.string, age: T.number }))
|
|
505
|
+
* ```
|
|
506
|
+
* @public
|
|
507
|
+
*/
|
|
224
508
|
export class ArrayOfValidator<T> extends Validator<T[]> {
|
|
509
|
+
/**
|
|
510
|
+
* Creates a new ArrayOfValidator.
|
|
511
|
+
*
|
|
512
|
+
* itemValidator - Validator used to validate each array element
|
|
513
|
+
*/
|
|
225
514
|
constructor(readonly itemValidator: Validatable<T>) {
|
|
226
515
|
super(
|
|
227
516
|
(value) => {
|
|
@@ -259,6 +548,18 @@ export class ArrayOfValidator<T> extends Validator<T[]> {
|
|
|
259
548
|
)
|
|
260
549
|
}
|
|
261
550
|
|
|
551
|
+
/**
|
|
552
|
+
* Returns a new validator that ensures the array is not empty.
|
|
553
|
+
*
|
|
554
|
+
* @returns A new validator that rejects empty arrays
|
|
555
|
+
* @throws ValidationError When the array is empty
|
|
556
|
+
* @example
|
|
557
|
+
* ```ts
|
|
558
|
+
* const nonEmptyStrings = T.arrayOf(T.string).nonEmpty()
|
|
559
|
+
* nonEmptyStrings.validate(["hello"]) // Valid
|
|
560
|
+
* nonEmptyStrings.validate([]) // Throws ValidationError
|
|
561
|
+
* ```
|
|
562
|
+
*/
|
|
262
563
|
nonEmpty() {
|
|
263
564
|
return this.check((value) => {
|
|
264
565
|
if (value.length === 0) {
|
|
@@ -267,6 +568,18 @@ export class ArrayOfValidator<T> extends Validator<T[]> {
|
|
|
267
568
|
})
|
|
268
569
|
}
|
|
269
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Returns a new validator that ensures the array has more than one element.
|
|
573
|
+
*
|
|
574
|
+
* @returns A new validator that requires at least 2 elements
|
|
575
|
+
* @throws ValidationError When the array has 1 or fewer elements
|
|
576
|
+
* @example
|
|
577
|
+
* ```ts
|
|
578
|
+
* const multipleItems = T.arrayOf(T.string).lengthGreaterThan1()
|
|
579
|
+
* multipleItems.validate(["a", "b"]) // Valid
|
|
580
|
+
* multipleItems.validate(["a"]) // Throws ValidationError
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
270
583
|
lengthGreaterThan1() {
|
|
271
584
|
return this.check((value) => {
|
|
272
585
|
if (value.length <= 1) {
|
|
@@ -276,8 +589,33 @@ export class ArrayOfValidator<T> extends Validator<T[]> {
|
|
|
276
589
|
}
|
|
277
590
|
}
|
|
278
591
|
|
|
279
|
-
/**
|
|
592
|
+
/**
|
|
593
|
+
* Validator for objects with a defined shape. Each property is validated using its corresponding
|
|
594
|
+
* validator from the config object. Can be configured to allow or reject unknown properties.
|
|
595
|
+
*
|
|
596
|
+
* @example
|
|
597
|
+
* ```ts
|
|
598
|
+
* const userValidator = new ObjectValidator({
|
|
599
|
+
* name: T.string,
|
|
600
|
+
* age: T.number,
|
|
601
|
+
* email: T.string.optional()
|
|
602
|
+
* })
|
|
603
|
+
*
|
|
604
|
+
* const user = userValidator.validate({
|
|
605
|
+
* name: "Alice",
|
|
606
|
+
* age: 25,
|
|
607
|
+
* email: "alice@example.com"
|
|
608
|
+
* })
|
|
609
|
+
* ```
|
|
610
|
+
* @public
|
|
611
|
+
*/
|
|
280
612
|
export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
|
613
|
+
/**
|
|
614
|
+
* Creates a new ObjectValidator.
|
|
615
|
+
*
|
|
616
|
+
* config - Object mapping property names to their validators
|
|
617
|
+
* shouldAllowUnknownProperties - Whether to allow properties not defined in config
|
|
618
|
+
*/
|
|
281
619
|
constructor(
|
|
282
620
|
public readonly config: {
|
|
283
621
|
readonly [K in keyof Shape]: Validatable<Shape[K]>
|
|
@@ -353,22 +691,33 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
|
|
353
691
|
)
|
|
354
692
|
}
|
|
355
693
|
|
|
694
|
+
/**
|
|
695
|
+
* Returns a new validator that allows unknown properties in the validated object.
|
|
696
|
+
*
|
|
697
|
+
* @returns A new ObjectValidator that accepts extra properties
|
|
698
|
+
* @example
|
|
699
|
+
* ```ts
|
|
700
|
+
* const flexibleUser = T.object({ name: T.string }).allowUnknownProperties()
|
|
701
|
+
* flexibleUser.validate({ name: "Alice", extra: "allowed" }) // Valid
|
|
702
|
+
* ```
|
|
703
|
+
*/
|
|
356
704
|
allowUnknownProperties() {
|
|
357
705
|
return new ObjectValidator(this.config, true)
|
|
358
706
|
}
|
|
359
707
|
|
|
360
708
|
/**
|
|
361
|
-
*
|
|
709
|
+
* Creates a new ObjectValidator by extending this validator with additional properties.
|
|
362
710
|
*
|
|
711
|
+
* @param extension - Object mapping new property names to their validators
|
|
712
|
+
* @returns A new ObjectValidator that validates both original and extended properties
|
|
363
713
|
* @example
|
|
364
|
-
*
|
|
365
714
|
* ```ts
|
|
366
|
-
* const
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
* meowVolume: T.number,
|
|
715
|
+
* const baseUser = T.object({ name: T.string, age: T.number })
|
|
716
|
+
* const adminUser = baseUser.extend({
|
|
717
|
+
* permissions: T.arrayOf(T.string),
|
|
718
|
+
* isAdmin: T.boolean
|
|
371
719
|
* })
|
|
720
|
+
* // adminUser validates: { name: string; age: number; permissions: string[]; isAdmin: boolean }
|
|
372
721
|
* ```
|
|
373
722
|
*/
|
|
374
723
|
extend<Extension extends Record<string, unknown>>(extension: {
|
|
@@ -380,19 +729,53 @@ export class ObjectValidator<Shape extends object> extends Validator<Shape> {
|
|
|
380
729
|
}
|
|
381
730
|
}
|
|
382
731
|
|
|
383
|
-
|
|
384
|
-
|
|
732
|
+
/**
|
|
733
|
+
* Configuration type for union validators. Each variant must be a validator that produces
|
|
734
|
+
* an object with the discriminator key set to the variant name.
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* ```ts
|
|
738
|
+
* type ShapeConfig = UnionValidatorConfig<'type', {
|
|
739
|
+
* circle: Validatable<{ type: 'circle'; radius: number }>
|
|
740
|
+
* square: Validatable<{ type: 'square'; size: number }>
|
|
741
|
+
* }>
|
|
742
|
+
* ```
|
|
743
|
+
* @public
|
|
744
|
+
*/
|
|
385
745
|
export type UnionValidatorConfig<Key extends string, Config> = {
|
|
386
746
|
readonly [Variant in keyof Config]: Validatable<any> & {
|
|
387
747
|
validate(input: any): { readonly [K in Key]: Variant }
|
|
388
748
|
}
|
|
389
749
|
}
|
|
390
|
-
/**
|
|
750
|
+
/**
|
|
751
|
+
* Validator for discriminated union types. Validates objects that can be one of several variants,
|
|
752
|
+
* distinguished by a discriminator property (key) that indicates which variant the object represents.
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* ```ts
|
|
756
|
+
* const shapeValidator = new UnionValidator('type', {
|
|
757
|
+
* circle: T.object({ type: T.literal('circle'), radius: T.number }),
|
|
758
|
+
* square: T.object({ type: T.literal('square'), size: T.number })
|
|
759
|
+
* }, () => { throw new Error('Unknown shape') }, false)
|
|
760
|
+
*
|
|
761
|
+
* const circle = shapeValidator.validate({ type: 'circle', radius: 5 })
|
|
762
|
+
* // circle is typed as { type: 'circle'; radius: number }
|
|
763
|
+
* ```
|
|
764
|
+
* @public
|
|
765
|
+
*/
|
|
391
766
|
export class UnionValidator<
|
|
392
767
|
Key extends string,
|
|
393
768
|
Config extends UnionValidatorConfig<Key, Config>,
|
|
394
769
|
UnknownValue = never,
|
|
395
770
|
> extends Validator<TypeOf<Config[keyof Config]> | UnknownValue> {
|
|
771
|
+
/**
|
|
772
|
+
* Creates a new UnionValidator.
|
|
773
|
+
*
|
|
774
|
+
* key - The discriminator property name used to determine the variant
|
|
775
|
+
* config - Object mapping variant names to their validators
|
|
776
|
+
* unknownValueValidation - Function to handle unknown variants
|
|
777
|
+
* useNumberKeys - Whether the discriminator uses number keys instead of strings
|
|
778
|
+
*/
|
|
396
779
|
constructor(
|
|
397
780
|
private readonly key: Key,
|
|
398
781
|
private readonly config: Config,
|
|
@@ -458,6 +841,20 @@ export class UnionValidator<
|
|
|
458
841
|
return { matchingSchema, variant }
|
|
459
842
|
}
|
|
460
843
|
|
|
844
|
+
/**
|
|
845
|
+
* Returns a new UnionValidator that can handle unknown variants using the provided function.
|
|
846
|
+
*
|
|
847
|
+
* @param unknownValueValidation - Function to validate/transform unknown variants
|
|
848
|
+
* @returns A new UnionValidator that accepts unknown variants
|
|
849
|
+
* @example
|
|
850
|
+
* ```ts
|
|
851
|
+
* const shapeValidator = T.union('type', { circle: circleValidator })
|
|
852
|
+
* .validateUnknownVariants((obj, variant) => {
|
|
853
|
+
* console.warn(`Unknown shape type: ${variant}`)
|
|
854
|
+
* return obj as UnknownShape
|
|
855
|
+
* })
|
|
856
|
+
* ```
|
|
857
|
+
*/
|
|
461
858
|
validateUnknownVariants<Unknown>(
|
|
462
859
|
unknownValueValidation: (value: object, variant: string) => Unknown
|
|
463
860
|
): UnionValidator<Key, Config, Unknown> {
|
|
@@ -465,8 +862,29 @@ export class UnionValidator<
|
|
|
465
862
|
}
|
|
466
863
|
}
|
|
467
864
|
|
|
468
|
-
/**
|
|
865
|
+
/**
|
|
866
|
+
* Validator for dictionary/map objects where both keys and values are validated.
|
|
867
|
+
* Useful for validating objects used as key-value stores.
|
|
868
|
+
*
|
|
869
|
+
* @example
|
|
870
|
+
* ```ts
|
|
871
|
+
* const scoreDict = new DictValidator(T.string, T.number)
|
|
872
|
+
* const scores = scoreDict.validate({
|
|
873
|
+
* "alice": 100,
|
|
874
|
+
* "bob": 85,
|
|
875
|
+
* "charlie": 92
|
|
876
|
+
* })
|
|
877
|
+
* // scores is typed as Record<string, number>
|
|
878
|
+
* ```
|
|
879
|
+
* @public
|
|
880
|
+
*/
|
|
469
881
|
export class DictValidator<Key extends string, Value> extends Validator<Record<Key, Value>> {
|
|
882
|
+
/**
|
|
883
|
+
* Creates a new DictValidator.
|
|
884
|
+
*
|
|
885
|
+
* keyValidator - Validator for object keys
|
|
886
|
+
* valueValidator - Validator for object values
|
|
887
|
+
*/
|
|
470
888
|
constructor(
|
|
471
889
|
public readonly keyValidator: Validatable<Key>,
|
|
472
890
|
public readonly valueValidator: Validatable<Value>
|
|
@@ -543,30 +961,51 @@ function typeofValidator<T>(type: string): Validator<T> {
|
|
|
543
961
|
}
|
|
544
962
|
|
|
545
963
|
/**
|
|
546
|
-
*
|
|
547
|
-
* validations.
|
|
964
|
+
* Validator that accepts any value without type checking. Useful as a starting point for
|
|
965
|
+
* building custom validations or when you need to accept truly unknown data.
|
|
548
966
|
*
|
|
967
|
+
* @example
|
|
968
|
+
* ```ts
|
|
969
|
+
* const result = T.unknown.validate(anything) // Returns the value as-is
|
|
970
|
+
* // result is typed as unknown
|
|
971
|
+
* ```
|
|
549
972
|
* @public
|
|
550
973
|
*/
|
|
551
974
|
export const unknown = new Validator((value) => value)
|
|
552
975
|
/**
|
|
553
|
-
*
|
|
554
|
-
*
|
|
976
|
+
* Validator that accepts any value and types it as 'any'. This should generally be avoided
|
|
977
|
+
* as it bypasses type safety, but can be used as an escape hatch for prototyping.
|
|
555
978
|
*
|
|
979
|
+
* @example
|
|
980
|
+
* ```ts
|
|
981
|
+
* const result = T.any.validate(anything) // Returns the value as any
|
|
982
|
+
* // result is typed as any - use with caution!
|
|
983
|
+
* ```
|
|
556
984
|
* @public
|
|
557
985
|
*/
|
|
558
986
|
export const any = new Validator((value): any => value)
|
|
559
987
|
|
|
560
988
|
/**
|
|
561
|
-
*
|
|
989
|
+
* Validator that ensures a value is a string.
|
|
562
990
|
*
|
|
991
|
+
* @example
|
|
992
|
+
* ```ts
|
|
993
|
+
* const name = T.string.validate("hello") // Returns "hello" as string
|
|
994
|
+
* T.string.validate(123) // Throws ValidationError: "Expected string, got a number"
|
|
995
|
+
* ```
|
|
563
996
|
* @public
|
|
564
997
|
*/
|
|
565
998
|
export const string = typeofValidator<string>('string')
|
|
566
999
|
|
|
567
1000
|
/**
|
|
568
|
-
*
|
|
1001
|
+
* Validator that ensures a value is a finite, non-NaN number. Rejects Infinity, -Infinity, and NaN.
|
|
569
1002
|
*
|
|
1003
|
+
* @example
|
|
1004
|
+
* ```ts
|
|
1005
|
+
* const count = T.number.validate(42) // Returns 42 as number
|
|
1006
|
+
* T.number.validate(NaN) // Throws ValidationError: "Expected a number, got NaN"
|
|
1007
|
+
* T.number.validate(Infinity) // Throws ValidationError: "Expected a finite number, got Infinity"
|
|
1008
|
+
* ```
|
|
570
1009
|
* @public
|
|
571
1010
|
*/
|
|
572
1011
|
export const number = typeofValidator<number>('number').check((number) => {
|
|
@@ -578,40 +1017,73 @@ export const number = typeofValidator<number>('number').check((number) => {
|
|
|
578
1017
|
}
|
|
579
1018
|
})
|
|
580
1019
|
/**
|
|
581
|
-
*
|
|
1020
|
+
* Validator that ensures a value is a non-negative number (\>= 0).
|
|
1021
|
+
* Despite the name "positive", this validator accepts zero.
|
|
582
1022
|
*
|
|
1023
|
+
* @example
|
|
1024
|
+
* ```ts
|
|
1025
|
+
* const price = T.positiveNumber.validate(29.99) // Returns 29.99
|
|
1026
|
+
* const free = T.positiveNumber.validate(0) // Returns 0 (valid)
|
|
1027
|
+
* T.positiveNumber.validate(-1) // Throws ValidationError: "Expected a positive number, got -1"
|
|
1028
|
+
* ```
|
|
583
1029
|
* @public
|
|
584
1030
|
*/
|
|
585
1031
|
export const positiveNumber = number.check((value) => {
|
|
586
1032
|
if (value < 0) throw new ValidationError(`Expected a positive number, got ${value}`)
|
|
587
1033
|
})
|
|
588
1034
|
/**
|
|
589
|
-
*
|
|
1035
|
+
* Validator that ensures a value is a positive number (\> 0). Rejects zero and negative numbers.
|
|
590
1036
|
*
|
|
1037
|
+
* @example
|
|
1038
|
+
* ```ts
|
|
1039
|
+
* const quantity = T.nonZeroNumber.validate(0.01) // Returns 0.01
|
|
1040
|
+
* T.nonZeroNumber.validate(0) // Throws ValidationError: "Expected a non-zero positive number, got 0"
|
|
1041
|
+
* T.nonZeroNumber.validate(-5) // Throws ValidationError: "Expected a non-zero positive number, got -5"
|
|
1042
|
+
* ```
|
|
591
1043
|
* @public
|
|
592
1044
|
*/
|
|
593
1045
|
export const nonZeroNumber = number.check((value) => {
|
|
594
1046
|
if (value <= 0) throw new ValidationError(`Expected a non-zero positive number, got ${value}`)
|
|
595
1047
|
})
|
|
596
1048
|
/**
|
|
597
|
-
*
|
|
1049
|
+
* Validator that ensures a value is an integer (whole number).
|
|
598
1050
|
*
|
|
1051
|
+
* @example
|
|
1052
|
+
* ```ts
|
|
1053
|
+
* const count = T.integer.validate(42) // Returns 42
|
|
1054
|
+
* T.integer.validate(3.14) // Throws ValidationError: "Expected an integer, got 3.14"
|
|
1055
|
+
* T.integer.validate(-5) // Returns -5 (negative integers are valid)
|
|
1056
|
+
* ```
|
|
599
1057
|
* @public
|
|
600
1058
|
*/
|
|
601
1059
|
export const integer = number.check((value) => {
|
|
602
1060
|
if (!Number.isInteger(value)) throw new ValidationError(`Expected an integer, got ${value}`)
|
|
603
1061
|
})
|
|
604
1062
|
/**
|
|
605
|
-
*
|
|
1063
|
+
* Validator that ensures a value is a non-negative integer (\>= 0).
|
|
1064
|
+
* Despite the name "positive", this validator accepts zero.
|
|
606
1065
|
*
|
|
1066
|
+
* @example
|
|
1067
|
+
* ```ts
|
|
1068
|
+
* const index = T.positiveInteger.validate(5) // Returns 5
|
|
1069
|
+
* const start = T.positiveInteger.validate(0) // Returns 0 (valid)
|
|
1070
|
+
* T.positiveInteger.validate(-1) // Throws ValidationError: "Expected a positive integer, got -1"
|
|
1071
|
+
* T.positiveInteger.validate(3.14) // Throws ValidationError: "Expected an integer, got 3.14"
|
|
1072
|
+
* ```
|
|
607
1073
|
* @public
|
|
608
1074
|
*/
|
|
609
1075
|
export const positiveInteger = integer.check((value) => {
|
|
610
1076
|
if (value < 0) throw new ValidationError(`Expected a positive integer, got ${value}`)
|
|
611
1077
|
})
|
|
612
1078
|
/**
|
|
613
|
-
*
|
|
1079
|
+
* Validator that ensures a value is a positive integer (\> 0). Rejects zero and negative integers.
|
|
614
1080
|
*
|
|
1081
|
+
* @example
|
|
1082
|
+
* ```ts
|
|
1083
|
+
* const itemCount = T.nonZeroInteger.validate(1) // Returns 1
|
|
1084
|
+
* T.nonZeroInteger.validate(0) // Throws ValidationError: "Expected a non-zero positive integer, got 0"
|
|
1085
|
+
* T.nonZeroInteger.validate(-5) // Throws ValidationError: "Expected a non-zero positive integer, got -5"
|
|
1086
|
+
* ```
|
|
615
1087
|
* @public
|
|
616
1088
|
*/
|
|
617
1089
|
export const nonZeroInteger = integer.check((value) => {
|
|
@@ -619,26 +1091,44 @@ export const nonZeroInteger = integer.check((value) => {
|
|
|
619
1091
|
})
|
|
620
1092
|
|
|
621
1093
|
/**
|
|
622
|
-
*
|
|
1094
|
+
* Validator that ensures a value is a boolean.
|
|
623
1095
|
*
|
|
1096
|
+
* @example
|
|
1097
|
+
* ```ts
|
|
1098
|
+
* const isActive = T.boolean.validate(true) // Returns true
|
|
1099
|
+
* const isEnabled = T.boolean.validate(false) // Returns false
|
|
1100
|
+
* T.boolean.validate("true") // Throws ValidationError: "Expected boolean, got a string"
|
|
1101
|
+
* ```
|
|
624
1102
|
* @public
|
|
625
1103
|
*/
|
|
626
1104
|
export const boolean = typeofValidator<boolean>('boolean')
|
|
627
1105
|
/**
|
|
628
|
-
*
|
|
1106
|
+
* Validator that ensures a value is a bigint.
|
|
629
1107
|
*
|
|
1108
|
+
* @example
|
|
1109
|
+
* ```ts
|
|
1110
|
+
* const largeNumber = T.bigint.validate(123n) // Returns 123n
|
|
1111
|
+
* T.bigint.validate(123) // Throws ValidationError: "Expected bigint, got a number"
|
|
1112
|
+
* ```
|
|
630
1113
|
* @public
|
|
631
1114
|
*/
|
|
632
1115
|
export const bigint = typeofValidator<bigint>('bigint')
|
|
633
1116
|
/**
|
|
634
|
-
*
|
|
1117
|
+
* Creates a validator that only accepts a specific literal value.
|
|
635
1118
|
*
|
|
1119
|
+
* @param expectedValue - The exact value that must be matched
|
|
1120
|
+
* @returns A validator that only accepts the specified literal value
|
|
1121
|
+
* @throws ValidationError When the value doesn't match the expected literal
|
|
636
1122
|
* @example
|
|
637
|
-
*
|
|
638
1123
|
* ```ts
|
|
639
1124
|
* const trueValidator = T.literal(true)
|
|
640
|
-
*
|
|
1125
|
+
* trueValidator.validate(true) // Returns true
|
|
1126
|
+
* trueValidator.validate(false) // Throws ValidationError
|
|
641
1127
|
*
|
|
1128
|
+
* const statusValidator = T.literal("active")
|
|
1129
|
+
* statusValidator.validate("active") // Returns "active"
|
|
1130
|
+
* statusValidator.validate("inactive") // Throws ValidationError
|
|
1131
|
+
* ```
|
|
642
1132
|
* @public
|
|
643
1133
|
*/
|
|
644
1134
|
export function literal<T extends string | number | boolean>(expectedValue: T): Validator<T> {
|
|
@@ -651,8 +1141,17 @@ export function literal<T extends string | number | boolean>(expectedValue: T):
|
|
|
651
1141
|
}
|
|
652
1142
|
|
|
653
1143
|
/**
|
|
654
|
-
*
|
|
1144
|
+
* Validator that ensures a value is an array. Does not validate the contents of the array.
|
|
1145
|
+
* Use T.arrayOf() to validate both the array structure and its contents.
|
|
655
1146
|
*
|
|
1147
|
+
* @example
|
|
1148
|
+
* ```ts
|
|
1149
|
+
* const items = T.array.validate([1, "hello", true]) // Returns unknown[]
|
|
1150
|
+
* T.array.validate("not array") // Throws ValidationError: "Expected an array, got a string"
|
|
1151
|
+
*
|
|
1152
|
+
* // For typed arrays, use T.arrayOf:
|
|
1153
|
+
* const numbers = T.arrayOf(T.number).validate([1, 2, 3]) // Returns number[]
|
|
1154
|
+
* ```
|
|
656
1155
|
* @public
|
|
657
1156
|
*/
|
|
658
1157
|
export const array = new Validator<unknown[]>((value) => {
|
|
@@ -663,15 +1162,37 @@ export const array = new Validator<unknown[]>((value) => {
|
|
|
663
1162
|
})
|
|
664
1163
|
|
|
665
1164
|
/**
|
|
666
|
-
*
|
|
1165
|
+
* Creates a validator for arrays where each element is validated using the provided validator.
|
|
667
1166
|
*
|
|
1167
|
+
* @param itemValidator - Validator to use for each array element
|
|
1168
|
+
* @returns An ArrayOfValidator that validates both array structure and element types
|
|
1169
|
+
* @throws ValidationError When the value is not an array or when any element is invalid
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```ts
|
|
1172
|
+
* const numberArray = T.arrayOf(T.number)
|
|
1173
|
+
* numberArray.validate([1, 2, 3]) // Returns number[]
|
|
1174
|
+
* numberArray.validate([1, "2", 3]) // Throws ValidationError at index 1
|
|
1175
|
+
*
|
|
1176
|
+
* const userArray = T.arrayOf(T.object({ name: T.string, age: T.number }))
|
|
1177
|
+
* ```
|
|
668
1178
|
* @public
|
|
669
1179
|
*/
|
|
670
1180
|
export function arrayOf<T>(itemValidator: Validatable<T>): ArrayOfValidator<T> {
|
|
671
1181
|
return new ArrayOfValidator(itemValidator)
|
|
672
1182
|
}
|
|
673
1183
|
|
|
674
|
-
/**
|
|
1184
|
+
/**
|
|
1185
|
+
* Validator that ensures a value is an object (non-null, non-array). Does not validate
|
|
1186
|
+
* the properties of the object.
|
|
1187
|
+
*
|
|
1188
|
+
* @example
|
|
1189
|
+
* ```ts
|
|
1190
|
+
* const obj = T.unknownObject.validate({ any: "properties" }) // Returns Record<string, unknown>
|
|
1191
|
+
* T.unknownObject.validate(null) // Throws ValidationError: "Expected object, got null"
|
|
1192
|
+
* T.unknownObject.validate([1, 2, 3]) // Throws ValidationError: "Expected object, got an array"
|
|
1193
|
+
* ```
|
|
1194
|
+
* @public
|
|
1195
|
+
*/
|
|
675
1196
|
export const unknownObject = new Validator<Record<string, unknown>>((value) => {
|
|
676
1197
|
if (typeof value !== 'object' || value === null) {
|
|
677
1198
|
throw new ValidationError(`Expected object, got ${typeToString(value)}`)
|
|
@@ -680,8 +1201,29 @@ export const unknownObject = new Validator<Record<string, unknown>>((value) => {
|
|
|
680
1201
|
})
|
|
681
1202
|
|
|
682
1203
|
/**
|
|
683
|
-
*
|
|
1204
|
+
* Creates a validator for objects with a defined shape. Each property is validated using
|
|
1205
|
+
* its corresponding validator from the config object.
|
|
684
1206
|
*
|
|
1207
|
+
* @param config - Object mapping property names to their validators
|
|
1208
|
+
* @returns An ObjectValidator that validates the object structure and all properties
|
|
1209
|
+
* @throws ValidationError When the value is not an object or when any property is invalid
|
|
1210
|
+
* @example
|
|
1211
|
+
* ```ts
|
|
1212
|
+
* const userValidator = T.object({
|
|
1213
|
+
* name: T.string,
|
|
1214
|
+
* age: T.number,
|
|
1215
|
+
* email: T.string.optional(),
|
|
1216
|
+
* isActive: T.boolean
|
|
1217
|
+
* })
|
|
1218
|
+
*
|
|
1219
|
+
* const user = userValidator.validate({
|
|
1220
|
+
* name: "Alice",
|
|
1221
|
+
* age: 25,
|
|
1222
|
+
* email: "alice@example.com",
|
|
1223
|
+
* isActive: true
|
|
1224
|
+
* })
|
|
1225
|
+
* // user is typed with full type safety
|
|
1226
|
+
* ```
|
|
685
1227
|
* @public
|
|
686
1228
|
*/
|
|
687
1229
|
export function object<Shape extends object>(config: {
|
|
@@ -722,8 +1264,15 @@ function isValidJson(value: any): value is JsonValue {
|
|
|
722
1264
|
}
|
|
723
1265
|
|
|
724
1266
|
/**
|
|
725
|
-
*
|
|
1267
|
+
* Validator that ensures a value is valid JSON (string, number, boolean, null, array, or plain object).
|
|
1268
|
+
* Rejects functions, undefined, symbols, and other non-JSON values.
|
|
726
1269
|
*
|
|
1270
|
+
* @example
|
|
1271
|
+
* ```ts
|
|
1272
|
+
* const data = T.jsonValue.validate({ name: "Alice", scores: [1, 2, 3], active: true })
|
|
1273
|
+
* T.jsonValue.validate(undefined) // Throws ValidationError
|
|
1274
|
+
* T.jsonValue.validate(() => {}) // Throws ValidationError
|
|
1275
|
+
* ```
|
|
727
1276
|
* @public
|
|
728
1277
|
*/
|
|
729
1278
|
export const jsonValue: Validator<JsonValue> = new Validator<JsonValue>(
|
|
@@ -786,8 +1335,19 @@ export const jsonValue: Validator<JsonValue> = new Validator<JsonValue>(
|
|
|
786
1335
|
)
|
|
787
1336
|
|
|
788
1337
|
/**
|
|
789
|
-
*
|
|
1338
|
+
* Creates a validator for JSON dictionaries (objects with string keys and JSON-serializable values).
|
|
790
1339
|
*
|
|
1340
|
+
* @returns A DictValidator that validates string keys and JSON values
|
|
1341
|
+
* @throws ValidationError When keys are not strings or values are not JSON-serializable
|
|
1342
|
+
* @example
|
|
1343
|
+
* ```ts
|
|
1344
|
+
* const config = T.jsonDict().validate({
|
|
1345
|
+
* "setting1": "value",
|
|
1346
|
+
* "setting2": 42,
|
|
1347
|
+
* "setting3": ["a", "b", "c"],
|
|
1348
|
+
* "setting4": { nested: true }
|
|
1349
|
+
* })
|
|
1350
|
+
* ```
|
|
791
1351
|
* @public
|
|
792
1352
|
*/
|
|
793
1353
|
export function jsonDict(): DictValidator<string, JsonValue> {
|
|
@@ -795,8 +1355,23 @@ export function jsonDict(): DictValidator<string, JsonValue> {
|
|
|
795
1355
|
}
|
|
796
1356
|
|
|
797
1357
|
/**
|
|
798
|
-
*
|
|
1358
|
+
* Creates a validator for dictionary objects where both keys and values are validated.
|
|
1359
|
+
* Useful for validating objects used as key-value maps.
|
|
799
1360
|
*
|
|
1361
|
+
* @param keyValidator - Validator for object keys
|
|
1362
|
+
* @param valueValidator - Validator for object values
|
|
1363
|
+
* @returns A DictValidator that validates all keys and values
|
|
1364
|
+
* @throws ValidationError When any key or value is invalid
|
|
1365
|
+
* @example
|
|
1366
|
+
* ```ts
|
|
1367
|
+
* const scores = T.dict(T.string, T.number)
|
|
1368
|
+
* scores.validate({ "alice": 100, "bob": 85 }) // Valid
|
|
1369
|
+
*
|
|
1370
|
+
* const userPrefs = T.dict(T.string, T.object({
|
|
1371
|
+
* theme: T.literalEnum('light', 'dark'),
|
|
1372
|
+
* notifications: T.boolean
|
|
1373
|
+
* }))
|
|
1374
|
+
* ```
|
|
800
1375
|
* @public
|
|
801
1376
|
*/
|
|
802
1377
|
export function dict<Key extends string, Value>(
|
|
@@ -807,17 +1382,24 @@ export function dict<Key extends string, Value>(
|
|
|
807
1382
|
}
|
|
808
1383
|
|
|
809
1384
|
/**
|
|
810
|
-
*
|
|
811
|
-
*
|
|
1385
|
+
* Creates a validator for discriminated union types. Validates objects that can be one of
|
|
1386
|
+
* several variants, distinguished by a discriminator property.
|
|
812
1387
|
*
|
|
1388
|
+
* @param key - The discriminator property name used to determine the variant
|
|
1389
|
+
* @param config - Object mapping variant names to their validators
|
|
1390
|
+
* @returns A UnionValidator that validates based on the discriminator value
|
|
1391
|
+
* @throws ValidationError When the discriminator is invalid or the variant validation fails
|
|
813
1392
|
* @example
|
|
814
|
-
*
|
|
815
1393
|
* ```ts
|
|
816
|
-
* const
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
1394
|
+
* const shapeValidator = T.union('type', {
|
|
1395
|
+
* circle: T.object({ type: T.literal('circle'), radius: T.number }),
|
|
1396
|
+
* square: T.object({ type: T.literal('square'), size: T.number }),
|
|
1397
|
+
* triangle: T.object({ type: T.literal('triangle'), base: T.number, height: T.number })
|
|
1398
|
+
* })
|
|
820
1399
|
*
|
|
1400
|
+
* const circle = shapeValidator.validate({ type: 'circle', radius: 5 })
|
|
1401
|
+
* // circle is typed as { type: 'circle'; radius: number }
|
|
1402
|
+
* ```
|
|
821
1403
|
* @public
|
|
822
1404
|
*/
|
|
823
1405
|
export function union<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(
|
|
@@ -840,6 +1422,13 @@ export function union<Key extends string, Config extends UnionValidatorConfig<Ke
|
|
|
840
1422
|
}
|
|
841
1423
|
|
|
842
1424
|
/**
|
|
1425
|
+
* Creates a validator for discriminated union types using number discriminators instead of strings.
|
|
1426
|
+
* This is an internal function used for specific cases where numeric discriminators are needed.
|
|
1427
|
+
*
|
|
1428
|
+
* @param key - The discriminator property name used to determine the variant
|
|
1429
|
+
* @param config - Object mapping variant names to their validators
|
|
1430
|
+
* @returns A UnionValidator that validates based on numeric discriminator values
|
|
1431
|
+
* @throws ValidationError When the discriminator is invalid or the variant validation fails
|
|
843
1432
|
* @internal
|
|
844
1433
|
*/
|
|
845
1434
|
export function numberUnion<Key extends string, Config extends UnionValidatorConfig<Key, Config>>(
|
|
@@ -862,9 +1451,23 @@ export function numberUnion<Key extends string, Config extends UnionValidatorCon
|
|
|
862
1451
|
}
|
|
863
1452
|
|
|
864
1453
|
/**
|
|
865
|
-
*
|
|
866
|
-
*
|
|
1454
|
+
* Creates a validator for named model objects with enhanced error reporting. The model name
|
|
1455
|
+
* will be included in error messages to provide better debugging context.
|
|
867
1456
|
*
|
|
1457
|
+
* @param name - The name of the model (used in error messages)
|
|
1458
|
+
* @param validator - The validator for the model structure
|
|
1459
|
+
* @returns A Validator with enhanced error reporting that includes the model name
|
|
1460
|
+
* @throws ValidationError With model name context when validation fails
|
|
1461
|
+
* @example
|
|
1462
|
+
* ```ts
|
|
1463
|
+
* const userModel = T.model('User', T.object({
|
|
1464
|
+
* id: T.string,
|
|
1465
|
+
* name: T.string,
|
|
1466
|
+
* email: T.linkUrl
|
|
1467
|
+
* }))
|
|
1468
|
+
*
|
|
1469
|
+
* // Error message will be: "At User.email: Expected a valid url, got 'invalid-email'"
|
|
1470
|
+
* ```
|
|
868
1471
|
* @public
|
|
869
1472
|
*/
|
|
870
1473
|
export function model<T extends { readonly id: string }>(
|
|
@@ -887,7 +1490,21 @@ export function model<T extends { readonly id: string }>(
|
|
|
887
1490
|
)
|
|
888
1491
|
}
|
|
889
1492
|
|
|
890
|
-
/**
|
|
1493
|
+
/**
|
|
1494
|
+
* Creates a validator that only accepts values from a given Set of allowed values.
|
|
1495
|
+
*
|
|
1496
|
+
* @param values - Set containing the allowed values
|
|
1497
|
+
* @returns A validator that only accepts values from the provided set
|
|
1498
|
+
* @throws ValidationError When the value is not in the allowed set
|
|
1499
|
+
* @example
|
|
1500
|
+
* ```ts
|
|
1501
|
+
* const allowedColors = new Set(['red', 'green', 'blue'] as const)
|
|
1502
|
+
* const colorValidator = T.setEnum(allowedColors)
|
|
1503
|
+
* colorValidator.validate('red') // Returns 'red'
|
|
1504
|
+
* colorValidator.validate('yellow') // Throws ValidationError
|
|
1505
|
+
* ```
|
|
1506
|
+
* @public
|
|
1507
|
+
*/
|
|
891
1508
|
export function setEnum<T>(values: ReadonlySet<T>): Validator<T> {
|
|
892
1509
|
return new Validator((value) => {
|
|
893
1510
|
if (!values.has(value as T)) {
|
|
@@ -898,7 +1515,20 @@ export function setEnum<T>(values: ReadonlySet<T>): Validator<T> {
|
|
|
898
1515
|
})
|
|
899
1516
|
}
|
|
900
1517
|
|
|
901
|
-
/**
|
|
1518
|
+
/**
|
|
1519
|
+
* Creates a validator that accepts either the validated type or undefined.
|
|
1520
|
+
*
|
|
1521
|
+
* @param validator - The base validator to make optional
|
|
1522
|
+
* @returns A validator that accepts T or undefined
|
|
1523
|
+
* @example
|
|
1524
|
+
* ```ts
|
|
1525
|
+
* const optionalString = T.optional(T.string)
|
|
1526
|
+
* optionalString.validate("hello") // Returns "hello"
|
|
1527
|
+
* optionalString.validate(undefined) // Returns undefined
|
|
1528
|
+
* optionalString.validate(null) // Throws ValidationError
|
|
1529
|
+
* ```
|
|
1530
|
+
* @public
|
|
1531
|
+
*/
|
|
902
1532
|
export function optional<T>(validator: Validatable<T>): Validator<T | undefined> {
|
|
903
1533
|
return new Validator(
|
|
904
1534
|
(value) => {
|
|
@@ -916,7 +1546,20 @@ export function optional<T>(validator: Validatable<T>): Validator<T | undefined>
|
|
|
916
1546
|
)
|
|
917
1547
|
}
|
|
918
1548
|
|
|
919
|
-
/**
|
|
1549
|
+
/**
|
|
1550
|
+
* Creates a validator that accepts either the validated type or null.
|
|
1551
|
+
*
|
|
1552
|
+
* @param validator - The base validator to make nullable
|
|
1553
|
+
* @returns A validator that accepts T or null
|
|
1554
|
+
* @example
|
|
1555
|
+
* ```ts
|
|
1556
|
+
* const nullableString = T.nullable(T.string)
|
|
1557
|
+
* nullableString.validate("hello") // Returns "hello"
|
|
1558
|
+
* nullableString.validate(null) // Returns null
|
|
1559
|
+
* nullableString.validate(undefined) // Throws ValidationError
|
|
1560
|
+
* ```
|
|
1561
|
+
* @public
|
|
1562
|
+
*/
|
|
920
1563
|
export function nullable<T>(validator: Validatable<T>): Validator<T | null> {
|
|
921
1564
|
return new Validator(
|
|
922
1565
|
(value) => {
|
|
@@ -933,7 +1576,21 @@ export function nullable<T>(validator: Validatable<T>): Validator<T | null> {
|
|
|
933
1576
|
)
|
|
934
1577
|
}
|
|
935
1578
|
|
|
936
|
-
/**
|
|
1579
|
+
/**
|
|
1580
|
+
* Creates a validator that only accepts one of the provided literal values.
|
|
1581
|
+
* This is a convenience function that creates a setEnum from the provided values.
|
|
1582
|
+
*
|
|
1583
|
+
* @param values - The allowed literal values
|
|
1584
|
+
* @returns A validator that only accepts the provided literal values
|
|
1585
|
+
* @throws ValidationError When the value is not one of the allowed literals
|
|
1586
|
+
* @example
|
|
1587
|
+
* ```ts
|
|
1588
|
+
* const themeValidator = T.literalEnum('light', 'dark', 'auto')
|
|
1589
|
+
* themeValidator.validate('light') // Returns 'light'
|
|
1590
|
+
* themeValidator.validate('blue') // Throws ValidationError: Expected "light" or "dark" or "auto", got blue
|
|
1591
|
+
* ```
|
|
1592
|
+
* @public
|
|
1593
|
+
*/
|
|
937
1594
|
export function literalEnum<const Values extends readonly unknown[]>(
|
|
938
1595
|
...values: Values
|
|
939
1596
|
): Validator<Values[number]> {
|
|
@@ -958,8 +1615,16 @@ function parseUrl(str: string) {
|
|
|
958
1615
|
const validLinkProtocols = new Set(['http:', 'https:', 'mailto:'])
|
|
959
1616
|
|
|
960
1617
|
/**
|
|
961
|
-
*
|
|
1618
|
+
* Validator for URLs that are safe to use as user-facing links. Accepts http, https, and mailto protocols.
|
|
1619
|
+
* This validator provides security by rejecting potentially dangerous protocols like javascript:.
|
|
962
1620
|
*
|
|
1621
|
+
* @example
|
|
1622
|
+
* ```ts
|
|
1623
|
+
* const link = T.linkUrl.validate("https://example.com") // Valid
|
|
1624
|
+
* const email = T.linkUrl.validate("mailto:user@example.com") // Valid
|
|
1625
|
+
* T.linkUrl.validate("") // Valid (empty string allowed)
|
|
1626
|
+
* T.linkUrl.validate("javascript:alert(1)") // Throws ValidationError (unsafe protocol)
|
|
1627
|
+
* ```
|
|
963
1628
|
* @public
|
|
964
1629
|
*/
|
|
965
1630
|
export const linkUrl = string.check((value) => {
|
|
@@ -977,8 +1642,16 @@ export const linkUrl = string.check((value) => {
|
|
|
977
1642
|
const validSrcProtocols = new Set(['http:', 'https:', 'data:', 'asset:'])
|
|
978
1643
|
|
|
979
1644
|
/**
|
|
980
|
-
*
|
|
1645
|
+
* Validator for URLs that are safe to use as asset sources. Accepts http, https, data, and asset protocols.
|
|
1646
|
+
* The asset: protocol refers to tldraw's local IndexedDB object store.
|
|
981
1647
|
*
|
|
1648
|
+
* @example
|
|
1649
|
+
* ```ts
|
|
1650
|
+
* const imageUrl = T.srcUrl.validate("https://example.com/image.png") // Valid
|
|
1651
|
+
* const dataUrl = T.srcUrl.validate("data:image/png;base64,iVBORw0...") // Valid
|
|
1652
|
+
* const assetUrl = T.srcUrl.validate("asset:abc123") // Valid (local asset reference)
|
|
1653
|
+
* T.srcUrl.validate("") // Valid (empty string allowed)
|
|
1654
|
+
* ```
|
|
982
1655
|
* @public
|
|
983
1656
|
*/
|
|
984
1657
|
export const srcUrl = string.check((value) => {
|
|
@@ -993,8 +1666,15 @@ export const srcUrl = string.check((value) => {
|
|
|
993
1666
|
})
|
|
994
1667
|
|
|
995
1668
|
/**
|
|
996
|
-
*
|
|
1669
|
+
* Validator for HTTP and HTTPS URLs only. Rejects all other protocols.
|
|
997
1670
|
*
|
|
1671
|
+
* @example
|
|
1672
|
+
* ```ts
|
|
1673
|
+
* const apiUrl = T.httpUrl.validate("https://api.example.com") // Valid
|
|
1674
|
+
* const httpUrl = T.httpUrl.validate("http://localhost:3000") // Valid
|
|
1675
|
+
* T.httpUrl.validate("") // Valid (empty string allowed)
|
|
1676
|
+
* T.httpUrl.validate("ftp://files.example.com") // Throws ValidationError (not http/https)
|
|
1677
|
+
* ```
|
|
998
1678
|
* @public
|
|
999
1679
|
*/
|
|
1000
1680
|
export const httpUrl = string.check((value) => {
|
|
@@ -1009,7 +1689,15 @@ export const httpUrl = string.check((value) => {
|
|
|
1009
1689
|
})
|
|
1010
1690
|
|
|
1011
1691
|
/**
|
|
1012
|
-
*
|
|
1692
|
+
* Validator for IndexKey values used in tldraw's indexing system. An IndexKey is a string
|
|
1693
|
+
* that meets specific format requirements for use as a database index.
|
|
1694
|
+
*
|
|
1695
|
+
* @throws ValidationError When the string is not a valid IndexKey format
|
|
1696
|
+
* @example
|
|
1697
|
+
* ```ts
|
|
1698
|
+
* const key = T.indexKey.validate("valid_index_key") // Returns IndexKey
|
|
1699
|
+
* T.indexKey.validate("invalid key!") // Throws ValidationError (invalid format)
|
|
1700
|
+
* ```
|
|
1013
1701
|
* @public
|
|
1014
1702
|
*/
|
|
1015
1703
|
export const indexKey = string.refine<IndexKey>((key) => {
|
|
@@ -1022,8 +1710,20 @@ export const indexKey = string.refine<IndexKey>((key) => {
|
|
|
1022
1710
|
})
|
|
1023
1711
|
|
|
1024
1712
|
/**
|
|
1025
|
-
*
|
|
1713
|
+
* Creates a validator that accepts values matching either of two validators.
|
|
1714
|
+
* Tries the first validator, and if it fails, tries the second validator.
|
|
1026
1715
|
*
|
|
1716
|
+
* @param v1 - The first validator to try
|
|
1717
|
+
* @param v2 - The second validator to try if the first fails
|
|
1718
|
+
* @returns A validator that accepts values matching either validator
|
|
1719
|
+
* @throws ValidationError When the value matches neither validator (throws error from v2)
|
|
1720
|
+
* @example
|
|
1721
|
+
* ```ts
|
|
1722
|
+
* const stringOrNumber = T.or(T.string, T.number)
|
|
1723
|
+
* stringOrNumber.validate("hello") // Returns "hello" as string
|
|
1724
|
+
* stringOrNumber.validate(42) // Returns 42 as number
|
|
1725
|
+
* stringOrNumber.validate(true) // Throws ValidationError from number validator
|
|
1726
|
+
* ```
|
|
1027
1727
|
* @public
|
|
1028
1728
|
*/
|
|
1029
1729
|
export function or<T1, T2>(v1: Validatable<T1>, v2: Validatable<T2>): Validator<T1 | T2> {
|