0
# Constraints and Transformations
1
2
Add custom validation logic and data transformation to existing runtypes. These utilities allow you to extend basic type validation with domain-specific rules, branding, and data parsing.
3
4
## Capabilities
5
6
### Constraint
7
8
Adds custom validation logic to an existing runtype, narrowing its type.
9
10
```typescript { .api }
11
/**
12
* Adds custom validation constraints to a runtype
13
* @param underlying - Base runtype to constrain
14
* @param constraint - Assertion function that validates and narrows the type
15
* @example Constraint(String, s => { if (s.length < 3) throw "Too short"; })
16
*/
17
function Constraint<T, U extends T>(
18
underlying: Runtype<T>,
19
constraint: (x: T) => asserts x is U
20
): ConstraintRuntype<T, U>;
21
22
interface ConstraintRuntype<T, U> extends Runtype<U> {
23
tag: "constraint";
24
underlying: Runtype<T>;
25
constraint: (x: T) => asserts x is U;
26
}
27
```
28
29
**Usage Examples:**
30
31
```typescript
32
import { Constraint, String, Number } from "runtypes";
33
34
// String length constraints
35
const MinLength = (min: number) =>
36
Constraint(String, (s: string): asserts s is string => {
37
if (s.length < min) throw `String must be at least ${min} characters`;
38
});
39
40
const Username = MinLength(3);
41
const username = Username.check("alice"); // "alice"
42
43
// Numeric range constraints
44
const PositiveNumber = Constraint(Number, (n: number): asserts n is number => {
45
if (n <= 0) throw "Number must be positive";
46
});
47
48
const Age = Constraint(Number, (n: number): asserts n is number => {
49
if (n < 0 || n > 150) throw "Age must be between 0 and 150";
50
});
51
52
const age = Age.check(25); // 25
53
54
// Email validation
55
const Email = Constraint(String, (s: string): asserts s is string => {
56
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
57
if (!emailRegex.test(s)) throw "Invalid email format";
58
});
59
60
const email = Email.check("user@example.com"); // "user@example.com"
61
```
62
63
### Built-in Constraint Helpers
64
65
Use the convenient built-in methods on runtypes for common constraints.
66
67
```typescript
68
import { String, Number } from "runtypes";
69
70
// withConstraint - returns boolean or error message
71
const PositiveInteger = Number.withConstraint(n => n > 0 && Number.isInteger(n) || "Must be positive integer");
72
73
// withGuard - type predicate function
74
const NonEmptyString = String.withGuard((s): s is string => s.length > 0);
75
76
// withAssertion - assertion function
77
const ValidUrl = String.withAssertion((s): asserts s is string => {
78
try {
79
new URL(s);
80
} catch {
81
throw "Invalid URL";
82
}
83
});
84
85
// Usage
86
const count = PositiveInteger.check(5);
87
const name = NonEmptyString.check("Alice");
88
const url = ValidUrl.check("https://example.com");
89
```
90
91
### Brand
92
93
Adds nominal typing to create distinct types that are structurally identical but semantically different.
94
95
```typescript { .api }
96
/**
97
* Adds a brand to create nominal typing
98
* @param brand - Brand identifier string
99
* @param entity - Underlying runtype to brand
100
* @example Brand("UserId", String).check("user_123") // branded string
101
*/
102
function Brand<B extends string, T>(brand: B, entity: Runtype<T>): BrandRuntype<B, T>;
103
104
interface BrandRuntype<B, T> extends Runtype<T & Brand<B>> {
105
tag: "brand";
106
brand: B;
107
entity: Runtype<T>;
108
}
109
110
// Or use the built-in method
111
declare module "runtypes" {
112
interface Runtype<T> {
113
withBrand<B extends string>(brand: B): BrandRuntype<B, T>;
114
}
115
}
116
```
117
118
**Usage Examples:**
119
120
```typescript
121
import { Brand, String, Number } from "runtypes";
122
123
// Create branded types
124
const UserId = Brand("UserId", String);
125
const ProductId = Brand("ProductId", String);
126
const Price = Brand("Price", Number);
127
128
type UserIdType = Static<typeof UserId>; // string & Brand<"UserId">
129
type ProductIdType = Static<typeof ProductId>; // string & Brand<"ProductId">
130
type PriceType = Static<typeof Price>; // number & Brand<"Price">
131
132
// Values are runtime identical but type-distinct
133
const userId = UserId.check("user_123");
134
const productId = ProductId.check("prod_456");
135
const price = Price.check(29.99);
136
137
// Type safety - these would be TypeScript errors:
138
// function processUser(id: UserIdType) { ... }
139
// processUser(productId); // Error: ProductId is not assignable to UserId
140
141
// Using withBrand method
142
const Email = String.withBrand("Email");
143
const Password = String.withBrand("Password");
144
145
// Combining with constraints
146
const ValidatedEmail = String
147
.withConstraint(s => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) || "Invalid email")
148
.withBrand("Email");
149
150
const StrongPassword = String
151
.withConstraint(s => s.length >= 8 || "Password must be at least 8 characters")
152
.withConstraint(s => /[A-Z]/.test(s) || "Password must contain uppercase letter")
153
.withConstraint(s => /[0-9]/.test(s) || "Password must contain number")
154
.withBrand("StrongPassword");
155
```
156
157
### Parser
158
159
Transforms validated values using a custom parser function, enabling data conversion and normalization.
160
161
```typescript { .api }
162
/**
163
* Adds custom parser to transform validated values
164
* @param underlying - Base runtype for validation
165
* @param parser - Function to transform the validated value
166
* @example Parser(String, s => s.toUpperCase()).parse("hello") // "HELLO"
167
*/
168
function Parser<T, U>(underlying: Runtype<T>, parser: (value: T) => U): ParserRuntype<T, U>;
169
170
interface ParserRuntype<T, U> extends Runtype<U> {
171
tag: "parser";
172
underlying: Runtype<T>;
173
parser: (value: T) => U;
174
}
175
176
// Or use the built-in method
177
declare module "runtypes" {
178
interface Runtype<T> {
179
withParser<U>(parser: (value: T) => U): ParserRuntype<T, U>;
180
}
181
}
182
```
183
184
**Usage Examples:**
185
186
```typescript
187
import { Parser, String, Number, Array, Object } from "runtypes";
188
189
// String transformations
190
const UpperCaseString = Parser(String, s => s.toUpperCase());
191
const TrimmedString = Parser(String, s => s.trim());
192
193
const name = UpperCaseString.parse("alice"); // "ALICE"
194
const cleaned = TrimmedString.parse(" hello "); // "hello"
195
196
// Numeric transformations
197
const IntegerFromString = Parser(String, s => parseInt(s, 10));
198
const AbsoluteNumber = Parser(Number, n => Math.abs(n));
199
200
const parsed = IntegerFromString.parse("123"); // 123 (number)
201
const absolute = AbsoluteNumber.parse(-42); // 42
202
203
// Date parsing
204
const DateFromString = Parser(String, s => new Date(s));
205
const ISODateFromTimestamp = Parser(Number, n => new Date(n).toISOString());
206
207
const date = DateFromString.parse("2024-01-15"); // Date object
208
const isoString = ISODateFromTimestamp.parse(1642204800000); // "2022-01-15T00:00:00.000Z"
209
210
// Complex object transformation
211
const UserInput = Object({
212
firstName: String,
213
lastName: String,
214
age: String, // Input as string
215
tags: String // Comma-separated string
216
});
217
218
const User = Parser(UserInput, input => ({
219
fullName: `${input.firstName} ${input.lastName}`,
220
age: parseInt(input.age, 10),
221
tags: input.tags.split(",").map(tag => tag.trim()),
222
createdAt: new Date()
223
}));
224
225
const userData = User.parse({
226
firstName: "Alice",
227
lastName: "Smith",
228
age: "25",
229
tags: "developer, typescript, nodejs"
230
});
231
// {
232
// fullName: "Alice Smith",
233
// age: 25,
234
// tags: ["developer", "typescript", "nodejs"],
235
// createdAt: Date
236
// }
237
```
238
239
### Chaining Constraints and Transformations
240
241
```typescript
242
import { String, Number } from "runtypes";
243
244
// Chain multiple operations
245
const ProcessedUsername = String
246
.withConstraint(s => s.length >= 3 || "Username too short")
247
.withParser(s => s.toLowerCase())
248
.withParser(s => s.replace(/[^a-z0-9]/g, ""))
249
.withConstraint(s => s.length > 0 || "Username cannot be empty after processing")
250
.withBrand("Username");
251
252
const username = ProcessedUsername.parse("Alice_123!"); // "alice123" (branded)
253
254
// Numeric processing chain
255
const Currency = Number
256
.withConstraint(n => n >= 0 || "Amount cannot be negative")
257
.withParser(n => Math.round(n * 100) / 100) // Round to 2 decimal places
258
.withBrand("Currency");
259
260
const price = Currency.parse(19.999); // 20.00 (branded)
261
```
262
263
## Advanced Constraint Patterns
264
265
### Custom Validation with Context
266
267
```typescript
268
import { Constraint, String, Object } from "runtypes";
269
270
// Validation that depends on other fields
271
const UserRegistration = Object({
272
username: String,
273
password: String,
274
confirmPassword: String
275
}).withConstraint((user): asserts user is any => {
276
if (user.password !== user.confirmPassword) {
277
throw "Passwords do not match";
278
}
279
if (user.password.includes(user.username)) {
280
throw "Password cannot contain username";
281
}
282
});
283
284
// Conditional validation
285
const ConditionalField = Object({
286
type: Union(Literal("premium"), Literal("basic")),
287
features: Array(String)
288
}).withConstraint((obj): asserts obj is any => {
289
if (obj.type === "premium" && obj.features.length === 0) {
290
throw "Premium type must have at least one feature";
291
}
292
});
293
```
294
295
### Dynamic Constraints
296
297
```typescript
298
import { Constraint, String, Number } from "runtypes";
299
300
// Factory for creating range constraints
301
const createRange = (min: number, max: number) =>
302
Number.withConstraint(n =>
303
(n >= min && n <= max) || `Must be between ${min} and ${max}`
304
);
305
306
const Percentage = createRange(0, 100);
307
const Rating = createRange(1, 5);
308
309
// Factory for string patterns
310
const createPattern = (pattern: RegExp, message: string) =>
311
String.withConstraint(s => pattern.test(s) || message);
312
313
const PhoneNumber = createPattern(/^\+?[\d\s-()]+$/, "Invalid phone number format");
314
const ZipCode = createPattern(/^\d{5}(-\d{4})?$/, "Invalid ZIP code format");
315
```
316
317
### Error Handling in Constraints
318
319
```typescript
320
import { Constraint, String, ValidationError } from "runtypes";
321
322
// Custom error types
323
class ValidationFailure extends Error {
324
constructor(public field: string, public value: unknown, message: string) {
325
super(message);
326
this.name = "ValidationFailure";
327
}
328
}
329
330
const StrictEmail = Constraint(String, (s: string): asserts s is string => {
331
const parts = s.split("@");
332
if (parts.length !== 2) {
333
throw new ValidationFailure("email", s, "Email must contain exactly one @ symbol");
334
}
335
336
const [local, domain] = parts;
337
if (!local || local.length === 0) {
338
throw new ValidationFailure("email.local", local, "Email local part cannot be empty");
339
}
340
341
if (!domain || !domain.includes(".")) {
342
throw new ValidationFailure("email.domain", domain, "Email domain must contain a dot");
343
}
344
});
345
346
// Usage with error handling
347
try {
348
const email = StrictEmail.check("invalid-email");
349
} catch (error) {
350
if (error instanceof ValidationError) {
351
console.log("Validation failed:", error.failure.message);
352
} else if (error instanceof ValidationFailure) {
353
console.log(`Field ${error.field} failed:`, error.message);
354
}
355
}
356
```