0
# Validation Methods
1
2
Every runtype provides essential validation methods for different use cases. These methods form the core API for runtime type checking and offer different approaches depending on whether you need exceptions, type guards, or detailed results.
3
4
## Capabilities
5
6
### Core Validation Methods
7
8
Every runtype implements these fundamental validation methods.
9
10
```typescript { .api }
11
/**
12
* Core validation methods available on every runtype
13
*/
14
interface Runtype<T> {
15
/**
16
* Validates and returns the value, throwing ValidationError on failure
17
* @param x - Value to validate
18
* @returns Validated value with proper typing
19
* @throws ValidationError when validation fails
20
*/
21
check<U>(x: U): T & U;
22
23
/**
24
* Type guard that returns boolean indicating validation success
25
* @param x - Value to validate
26
* @returns True if valid, false otherwise (no exceptions)
27
*/
28
guard<U>(x: U): x is T & U;
29
30
/**
31
* Assertion function that throws ValidationError on failure
32
* @param x - Value to validate
33
* @throws ValidationError when validation fails
34
*/
35
assert<U>(x: U): asserts x is T & U;
36
37
/**
38
* Validates and parses the value, applying transformations
39
* @param x - Value to validate and parse
40
* @returns Parsed/transformed value
41
* @throws ValidationError when validation fails
42
*/
43
parse<U>(x: U): T;
44
45
/**
46
* Detailed validation with structured result (no exceptions)
47
* @param x - Value to validate
48
* @param options - Validation options
49
* @returns Result object with success/failure details
50
*/
51
inspect<U>(x: U, options?: { parse?: boolean }): Result<T>;
52
}
53
```
54
55
### check - Validated Value with Exceptions
56
57
The most commonly used validation method. Returns the validated value or throws an exception.
58
59
```typescript
60
import { String, Number, Object, Array } from "runtypes";
61
62
// Basic usage
63
const name = String.check("Alice"); // "Alice"
64
const age = Number.check(25); // 25
65
66
// Throws ValidationError on failure
67
try {
68
const invalid = String.check(123);
69
} catch (error) {
70
console.log("Validation failed:", error.message);
71
}
72
73
// Type narrowing with intersection
74
function processUserData(input: unknown) {
75
const userData = Object({
76
name: String,
77
age: Number,
78
email: String
79
}).check(input);
80
81
// userData is now properly typed
82
console.log(userData.name.toUpperCase()); // TypeScript knows name is string
83
console.log(userData.age + 10); // TypeScript knows age is number
84
}
85
86
// Array validation
87
const scores = Array(Number).check([95, 87, 92]); // number[]
88
scores.forEach(score => console.log(score * 1.1)); // TypeScript knows each score is number
89
```
90
91
### guard - Type Guard with Boolean Result
92
93
Returns a boolean and acts as a TypeScript type guard. Never throws exceptions.
94
95
```typescript
96
import { String, Number, Object, Union, Literal } from "runtypes";
97
98
// Basic type guarding
99
function processValue(input: unknown) {
100
if (String.guard(input)) {
101
// TypeScript knows input is string here
102
return input.toUpperCase();
103
}
104
105
if (Number.guard(input)) {
106
// TypeScript knows input is number here
107
return input * 2;
108
}
109
110
return "Unknown type";
111
}
112
113
// Object type guarding
114
const User = Object({
115
id: Number,
116
name: String,
117
active: Boolean
118
});
119
120
function handleUserData(data: unknown) {
121
if (User.guard(data)) {
122
// data is now typed as { id: number; name: string; active: boolean }
123
console.log(`User ${data.id}: ${data.name} (${data.active ? 'active' : 'inactive'})`);
124
} else {
125
console.log("Invalid user data");
126
}
127
}
128
129
// Union type discrimination
130
const Status = Union(Literal("pending"), Literal("approved"), Literal("rejected"));
131
132
function handleStatus(status: unknown) {
133
if (Status.guard(status)) {
134
// status is now typed as "pending" | "approved" | "rejected"
135
switch (status) {
136
case "pending":
137
console.log("Waiting for approval");
138
break;
139
case "approved":
140
console.log("Request approved");
141
break;
142
case "rejected":
143
console.log("Request rejected");
144
break;
145
}
146
}
147
}
148
```
149
150
### assert - Assertion Function
151
152
Assertion function that throws on failure but doesn't return a value. Useful for type narrowing.
153
154
```typescript
155
import { String, Number, Object } from "runtypes";
156
157
// Basic assertion
158
function processString(input: unknown) {
159
String.assert(input);
160
// input is now typed as string
161
return input.toLowerCase();
162
}
163
164
// Multiple assertions
165
function processUserInput(data: unknown) {
166
const UserData = Object({
167
name: String,
168
age: Number,
169
email: String
170
});
171
172
UserData.assert(data);
173
// data is now properly typed
174
175
console.log(`Processing user: ${data.name}`);
176
console.log(`Age: ${data.age}`);
177
console.log(`Email: ${data.email}`);
178
}
179
180
// Conditional assertions
181
function validateConfig(config: unknown) {
182
if (typeof config === "object" && config !== null) {
183
Object({
184
apiKey: String,
185
timeout: Number,
186
retries: Number.optional()
187
}).assert(config);
188
189
// config is now properly typed
190
return config;
191
}
192
193
throw new Error("Config must be an object");
194
}
195
```
196
197
### parse - Value Transformation
198
199
Validates and applies parsing/transformation. The difference from `check` is that `parse` applies any parsers in the runtype chain.
200
201
```typescript
202
import { String, Number, Parser, Object } from "runtypes";
203
204
// Basic parsing (same as check for simple types)
205
const name = String.parse("Alice"); // "Alice"
206
const age = Number.parse(25); // 25
207
208
// With parser transformations
209
const UppercaseString = String.withParser(s => s.toUpperCase());
210
const result = UppercaseString.parse("hello"); // "HELLO"
211
212
const DateFromString = String.withParser(s => new Date(s));
213
const date = DateFromString.parse("2024-01-15"); // Date object
214
215
// Complex object parsing
216
const UserInput = Object({
217
name: String,
218
age: String, // Input as string
219
preferences: String // JSON string
220
});
221
222
const UserProcessor = UserInput.withParser(input => ({
223
name: input.name.trim(),
224
age: parseInt(input.age, 10),
225
preferences: JSON.parse(input.preferences),
226
processedAt: new Date()
227
}));
228
229
const user = UserProcessor.parse({
230
name: " Alice ",
231
age: "25",
232
preferences: '{"theme": "dark", "notifications": true}'
233
});
234
// {
235
// name: "Alice",
236
// age: 25,
237
// preferences: { theme: "dark", notifications: true },
238
// processedAt: Date
239
// }
240
241
// Chained transformations
242
const ProcessedString = String
243
.withParser(s => s.trim())
244
.withParser(s => s.toLowerCase())
245
.withParser(s => s.replace(/\s+/g, "-"));
246
247
const slug = ProcessedString.parse(" Hello World "); // "hello-world"
248
```
249
250
### inspect - Detailed Results
251
252
Returns detailed Result object without throwing exceptions. Useful when you need comprehensive error information.
253
254
```typescript
255
import { String, Number, Object, Array, type Result } from "runtypes";
256
257
// Basic inspection
258
const stringResult = String.inspect("hello");
259
if (stringResult.success) {
260
console.log("Valid string:", stringResult.value);
261
} else {
262
console.log("Error:", stringResult.message);
263
console.log("Code:", stringResult.code);
264
}
265
266
// Parsing vs checking with inspect
267
const UppercaseString = String.withParser(s => s.toUpperCase());
268
269
// Without parsing
270
const checkResult = UppercaseString.inspect("hello", { parse: false });
271
// Returns original value if valid
272
273
// With parsing (default)
274
const parseResult = UppercaseString.inspect("hello", { parse: true });
275
// Returns transformed value if valid
276
277
// Complex validation with detailed errors
278
const User = Object({
279
profile: Object({
280
name: String,
281
age: Number
282
}),
283
settings: Object({
284
theme: Union(Literal("light"), Literal("dark")),
285
notifications: Boolean
286
})
287
});
288
289
function analyzeValidation(data: unknown): void {
290
const result = User.inspect(data);
291
292
if (result.success) {
293
console.log("✓ Validation successful");
294
return;
295
}
296
297
console.log("✗ Validation failed:");
298
console.log(`Main error: ${result.code} - ${result.message}`);
299
300
if (result.details) {
301
console.log("Detailed errors:");
302
analyzeDetails(result.details, "");
303
}
304
}
305
306
function analyzeDetails(details: Record<string, any>, path: string) {
307
for (const [key, detail] of Object.entries(details)) {
308
const currentPath = path ? `${path}.${key}` : key;
309
310
if (!detail.success) {
311
console.log(` ${currentPath}: ${detail.code} - ${detail.message}`);
312
313
if (detail.details) {
314
analyzeDetails(detail.details, currentPath);
315
}
316
}
317
}
318
}
319
320
// Usage
321
analyzeValidation({
322
profile: {
323
name: "Alice",
324
age: "not a number" // Error
325
},
326
settings: {
327
theme: "invalid", // Error
328
notifications: true
329
}
330
});
331
```
332
333
## Runtype Composition and Modification Methods
334
335
Beyond validation, every runtype provides methods for composition, constraints, and transformations.
336
337
### Composition Methods
338
339
Combine runtypes using logical operations.
340
341
```typescript { .api }
342
/**
343
* Composition methods for combining runtypes
344
*/
345
interface Runtype<T> {
346
/**
347
* Create union type (logical OR)
348
* @param other - Another runtype to union with
349
* @returns Union runtype accepting either type
350
*/
351
or<R extends Runtype.Core>(other: R): Union<[this, R]>;
352
353
/**
354
* Create intersection type (logical AND)
355
* @param other - Another runtype to intersect with
356
* @returns Intersect runtype requiring both types
357
*/
358
and<R extends Runtype.Core>(other: R): Intersect<[this, R]>;
359
}
360
```
361
362
**Usage Examples:**
363
364
```typescript
365
import { String, Number, Object } from "runtypes";
366
367
// Union using .or()
368
const StringOrNumber = String.or(Number);
369
StringOrNumber.check("hello"); // "hello"
370
StringOrNumber.check(42); // 42
371
StringOrNumber.check(true); // throws ValidationError
372
373
// Intersection using .and()
374
const PersonBase = Object({ name: String });
375
const PersonWithAge = Object({ age: Number });
376
const CompletePerson = PersonBase.and(PersonWithAge);
377
378
CompletePerson.check({ name: "Alice", age: 30 }); // ✓
379
CompletePerson.check({ name: "Bob" }); // ✗ missing age
380
```
381
382
### Optional and Nullable Methods
383
384
Shortcuts for common nullable/optional patterns.
385
386
```typescript { .api }
387
/**
388
* Methods for optional and nullable types
389
*/
390
interface Runtype<T> {
391
/** Make property optional (can be absent) */
392
optional(): Optional<this, never>;
393
394
/** Make property optional with default value */
395
default<V = never>(value: V): Optional<this, V>;
396
397
/** Allow null values */
398
nullable(): Union<[this, Literal<null>]>;
399
400
/** Allow undefined values */
401
undefinedable(): Union<[this, Literal<undefined>]>;
402
403
/** Allow null or undefined values */
404
nullishable(): Union<[this, Literal<null>, Literal<undefined>]>;
405
}
406
```
407
408
**Usage Examples:**
409
410
```typescript
411
import { String, Number, Object } from "runtypes";
412
413
// Optional properties in objects
414
const User = Object({
415
name: String,
416
email: String,
417
age: Number.optional(), // Can be absent
418
bio: String.default("No bio provided"), // Default value if absent
419
avatar: String.nullable(), // Can be null
420
lastSeen: String.undefinedable(), // Can be undefined
421
metadata: String.nullishable() // Can be null or undefined
422
});
423
424
type UserType = Static<typeof User>;
425
// {
426
// name: string;
427
// email: string;
428
// age?: number;
429
// bio: string;
430
// avatar: string | null;
431
// lastSeen: string | undefined;
432
// metadata: string | null | undefined;
433
// }
434
```
435
436
### Constraint Methods
437
438
Add custom validation logic while preserving types.
439
440
```typescript { .api }
441
/**
442
* Methods for adding constraints and custom validation
443
*/
444
interface Runtype<T> {
445
/**
446
* Add constraint with boolean or error message return
447
* @param constraint - Function returning boolean or error string
448
* @returns Constrained runtype
449
*/
450
withConstraint<Y extends T>(constraint: (x: T) => boolean | string): Constraint<this, Y>;
451
452
/**
453
* Add type guard constraint
454
* @param guard - TypeScript type guard function
455
* @returns Constrained runtype with narrowed type
456
*/
457
withGuard<Y extends T>(guard: (x: T) => x is Y): Constraint<this, Y>;
458
459
/**
460
* Add assertion constraint
461
* @param assert - TypeScript assertion function
462
* @returns Constrained runtype with narrowed type
463
*/
464
withAssertion<Y extends T>(assert: (x: T) => asserts x is Y): Constraint<this, Y>;
465
}
466
```
467
468
**Usage Examples:**
469
470
```typescript
471
import { String, Number } from "runtypes";
472
473
// Constraint with boolean
474
const PositiveNumber = Number.withConstraint(n => n > 0);
475
476
// Constraint with error message
477
const NonEmptyString = String.withConstraint(
478
s => s.length > 0 || "String cannot be empty"
479
);
480
481
// Email validation with regex
482
const Email = String.withConstraint(
483
s => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) || "Invalid email format"
484
);
485
486
// Type guard constraint
487
const isPositive = (n: number): n is number => n > 0;
488
const PositiveWithGuard = Number.withGuard(isPositive);
489
490
// Assertion constraint
491
const assertIsEmail = (s: string): asserts s is string => {
492
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s)) {
493
throw new Error("Invalid email");
494
}
495
};
496
const EmailWithAssertion = String.withAssertion(assertIsEmail);
497
```
498
499
### Branding and Parsing Methods
500
501
Add semantic meaning and transformations to types.
502
503
```typescript { .api }
504
/**
505
* Methods for branding and parsing/transformation
506
*/
507
interface Runtype<T, X = T> {
508
/**
509
* Add brand to create nominal typing
510
* @param brand - Brand identifier string
511
* @returns Branded runtype
512
*/
513
withBrand<B extends string>(brand: B): Brand<B, this>;
514
515
/**
516
* Add transformation/parsing logic
517
* @param parser - Function to transform validated value
518
* @returns Parser runtype with transformed output type
519
*/
520
withParser<Y>(parser: (value: X) => Y): Parser<this, Y>;
521
}
522
```
523
524
**Usage Examples:**
525
526
```typescript
527
import { String, Number, type Static } from "runtypes";
528
529
// Branded types for domain modeling
530
const UserId = String.withBrand("UserId");
531
const ProductId = String.withBrand("ProductId");
532
533
type UserIdType = Static<typeof UserId>; // string & Brand<"UserId">
534
type ProductIdType = Static<typeof ProductId>; // string & Brand<"ProductId">
535
536
// These are now different types despite both being strings
537
const userId = UserId.check("user-123");
538
const productId = ProductId.check("prod-456");
539
540
// Type error: can't assign UserId to ProductId variable
541
// const wrongId: ProductIdType = userId; // ✗
542
543
// Parsing transformations
544
const DateFromString = String.withParser(s => new Date(s));
545
const UppercaseString = String.withParser(s => s.toUpperCase());
546
const NumberFromString = String.withParser(s => parseInt(s, 10));
547
548
// Chaining parsers
549
const PositiveIntFromString = String
550
.withParser(s => parseInt(s, 10))
551
.withConstraint(n => n > 0 || "Must be positive");
552
553
const result = PositiveIntFromString.parse("42"); // number (42)
554
```
555
556
### Extension and Utility Methods
557
558
Add custom properties and utilities to runtypes.
559
560
```typescript { .api }
561
/**
562
* Utility methods for extending and introspecting runtypes
563
*/
564
interface Runtype<T, X = T> {
565
/**
566
* Add custom properties to the runtype
567
* @param extension - Object or function returning properties to add
568
* @returns Extended runtype with additional properties
569
*/
570
with<P extends object>(extension: P | ((self: this) => P)): this & P;
571
572
/**
573
* Create a shallow clone of the runtype
574
* @returns Cloned runtype
575
*/
576
clone(): this;
577
578
/**
579
* Ensure runtype conforms to a specific TypeScript type
580
* @returns Runtype with type conformance checking
581
*/
582
conform<V, Y = V>(this: Conform<V, Y>): Conform<V, Y> & this;
583
}
584
585
/**
586
* Static methods on the Runtype constructor
587
*/
588
interface RuntypeStatic {
589
/**
590
* Check if a value is a runtype
591
* @param x - Value to check
592
* @returns Type guard for runtype
593
*/
594
isRuntype(x: unknown): x is Runtype.Interfaces;
595
596
/**
597
* Assert that a value is a runtype
598
* @param x - Value to assert
599
* @throws If value is not a runtype
600
*/
601
assertIsRuntype(x: unknown): asserts x is Runtype.Interfaces;
602
}
603
```
604
605
**Usage Examples:**
606
607
```typescript
608
import { String, Number, Runtype } from "runtypes";
609
610
// Adding custom utilities to runtypes
611
const Username = String
612
.withConstraint(s => s.length >= 3 || "Username too short")
613
.with({
614
defaultValue: "anonymous",
615
validateUnique: async (username: string) => {
616
// Custom async validation logic
617
return !await userExists(username);
618
}
619
});
620
621
// Access custom properties
622
console.log(Username.defaultValue); // "anonymous"
623
await Username.validateUnique("newuser");
624
625
// Using function for extension (access to self)
626
const Temperature = Number.with(self => ({
627
celsius: (fahrenheit: number) => {
628
const celsius = (fahrenheit - 32) * 5/9;
629
return self.check(celsius);
630
},
631
fahrenheit: (celsius: number) => {
632
const fahrenheit = celsius * 9/5 + 32;
633
return self.check(fahrenheit);
634
}
635
}));
636
637
const temp = Temperature.celsius(100); // 37.77...
638
639
// Type conformance
640
interface User {
641
name: string;
642
age: number;
643
email?: string;
644
}
645
646
const UserRuntype = Object({
647
name: String,
648
age: Number,
649
email: String.optional()
650
}).conform<User>(); // Ensures exact match with User interface
651
652
// Static utility methods
653
const maybeRuntype: unknown = String;
654
655
if (Runtype.isRuntype(maybeRuntype)) {
656
// maybeRuntype is now typed as Runtype.Interfaces
657
const result = maybeRuntype.check("test");
658
}
659
660
// Assert for type narrowing
661
Runtype.assertIsRuntype(maybeRuntype); // Throws if not runtype
662
// maybeRuntype is now typed as Runtype.Interfaces
663
```
664
665
## Method Comparison and Use Cases
666
667
### When to Use Each Method
668
669
```typescript
670
import { String, Number, Object } from "runtypes";
671
672
const UserSchema = Object({
673
id: Number,
674
name: String,
675
email: String
676
});
677
678
// check() - Most common, when you want the validated value
679
function processUser(data: unknown) {
680
try {
681
const user = UserSchema.check(data);
682
return `User: ${user.name} (${user.email})`;
683
} catch (error) {
684
return "Invalid user data";
685
}
686
}
687
688
// guard() - Type narrowing in conditionals
689
function maybeProcessUser(data: unknown) {
690
if (UserSchema.guard(data)) {
691
// data is now typed, no exception handling needed
692
return `User: ${data.name} (${data.email})`;
693
}
694
return "Not a user";
695
}
696
697
// assert() - When you need type narrowing but not the return value
698
function validateAndProcess(data: unknown) {
699
UserSchema.assert(data); // Throws if invalid
700
// data is now typed as user object
701
702
// Continue processing with typed data
703
processUserEmail(data.email);
704
updateUserName(data.name);
705
}
706
707
// parse() - When transformations are involved
708
const UserWithTransform = UserSchema.withParser(user => ({
709
...user,
710
displayName: `${user.name} <${user.email}>`,
711
processedAt: new Date()
712
}));
713
714
function createDisplayUser(data: unknown) {
715
return UserWithTransform.parse(data); // Returns transformed object
716
}
717
718
// inspect() - When you need detailed error analysis
719
function validateWithReport(data: unknown) {
720
const result = UserSchema.inspect(data);
721
722
return {
723
isValid: result.success,
724
data: result.success ? result.value : null,
725
errors: result.success ? [] : collectErrors(result)
726
};
727
}
728
```
729
730
### Performance Considerations
731
732
```typescript
733
import { String, Object } from "runtypes";
734
735
const SimpleSchema = Object({ name: String, age: Number });
736
737
// guard() is fastest for boolean checks (no exception creation)
738
function fastCheck(data: unknown): boolean {
739
return SimpleSchema.guard(data);
740
}
741
742
// check() has exception overhead on failure
743
function checkWithTryCatch(data: unknown) {
744
try {
745
return SimpleSchema.check(data);
746
} catch {
747
return null;
748
}
749
}
750
751
// inspect() provides detailed results without exceptions
752
function detailedCheck(data: unknown) {
753
const result = SimpleSchema.inspect(data);
754
return result.success ? result.value : null;
755
}
756
757
// For hot paths where you expect mostly valid data, use check()
758
// For validation where failures are common, use guard() or inspect()
759
// For debugging and development, use inspect() for detailed feedback
760
```
761
762
### Error Handling Patterns
763
764
```typescript
765
import { ValidationError } from "runtypes";
766
767
// Pattern 1: Try-catch with check
768
function safeProcess(data: unknown) {
769
try {
770
const validated = UserSchema.check(data);
771
return { success: true, data: validated };
772
} catch (error) {
773
if (ValidationError.isValidationError(error)) {
774
return { success: false, error: error.message };
775
}
776
throw error; // Re-throw non-validation errors
777
}
778
}
779
780
// Pattern 2: Guard with fallback
781
function guardProcess(data: unknown) {
782
if (UserSchema.guard(data)) {
783
return processValidUser(data);
784
}
785
return getDefaultUser();
786
}
787
788
// Pattern 3: Inspect with detailed handling
789
function inspectProcess(data: unknown) {
790
const result = UserSchema.inspect(data);
791
792
if (result.success) {
793
return result.value;
794
}
795
796
// Handle specific error types
797
switch (result.code) {
798
case "TYPE_INCORRECT":
799
return handleTypeError(result);
800
case "PROPERTY_MISSING":
801
return handleMissingProperty(result);
802
case "CONTENT_INCORRECT":
803
return handleContentError(result);
804
default:
805
return handleGenericError(result);
806
}
807
}
808
```