0
# Utilities
1
2
Pattern matching and helper utilities for working with runtypes. These tools provide additional functionality for advanced validation scenarios and convenient type manipulation.
3
4
## Capabilities
5
6
### Pattern Matching
7
8
Pattern matching utilities for working with union types and discriminated unions.
9
10
```typescript { .api }
11
/**
12
* Creates a pattern matcher for union types
13
* @param cases - Array of case handlers created with when()
14
* @returns Function that matches value against cases
15
* @example match(when(String, s => s.length), when(Number, n => n * 2))
16
*/
17
function match<C extends readonly Case[]>(...cases: C): Matcher<C>;
18
19
/**
20
* Creates a case for pattern matching
21
* @param runtype - Runtype to match against
22
* @param transformer - Function to execute when matched
23
* @returns Case object for use with match()
24
* @example when(String, s => s.toUpperCase())
25
*/
26
function when<T, U>(runtype: Runtype<T>, transformer: (value: T) => U): Case<T, U>;
27
28
type Matcher<C> = (value: unknown) => ReturnType<C[number]>;
29
type Case<T, U> = [runtype: Runtype<T>, transformer: (value: T) => U];
30
```
31
32
**Usage Examples:**
33
34
```typescript
35
import { match, when, String, Number, Boolean, Object, Union, Literal } from "runtypes";
36
37
// Basic pattern matching
38
const processValue = match(
39
when(String, str => `String: ${str.toUpperCase()}`),
40
when(Number, num => `Number: ${num * 2}`),
41
when(Boolean, bool => `Boolean: ${bool ? "yes" : "no"}`)
42
);
43
44
console.log(processValue("hello")); // "String: HELLO"
45
console.log(processValue(42)); // "Number: 84"
46
console.log(processValue(true)); // "Boolean: yes"
47
48
// Discriminated union pattern matching
49
const Shape = Union(
50
Object({ type: Literal("circle"), radius: Number }),
51
Object({ type: Literal("rectangle"), width: Number, height: Number }),
52
Object({ type: Literal("triangle"), base: Number, height: Number })
53
);
54
55
const calculateArea = match(
56
when(Object({ type: Literal("circle"), radius: Number }),
57
({ radius }) => Math.PI * radius ** 2),
58
when(Object({ type: Literal("rectangle"), width: Number, height: Number }),
59
({ width, height }) => width * height),
60
when(Object({ type: Literal("triangle"), base: Number, height: Number }),
61
({ base, height }) => (base * height) / 2)
62
);
63
64
const area = calculateArea({ type: "circle", radius: 5 }); // ~78.54
65
```
66
67
### API Response Pattern Matching
68
69
```typescript
70
import { match, when, Object, Literal, String, Number, Array } from "runtypes";
71
72
// API response types
73
const SuccessResponse = Object({
74
status: Literal("success"),
75
data: Unknown
76
});
77
78
const ErrorResponse = Object({
79
status: Literal("error"),
80
message: String,
81
code: Number
82
});
83
84
const LoadingResponse = Object({
85
status: Literal("loading"),
86
progress: Number.optional()
87
});
88
89
// Pattern matcher for responses
90
const handleApiResponse = match(
91
when(SuccessResponse, ({ data }) => ({
92
type: "success" as const,
93
payload: data
94
})),
95
when(ErrorResponse, ({ message, code }) => ({
96
type: "error" as const,
97
error: `${code}: ${message}`
98
})),
99
when(LoadingResponse, ({ progress = 0 }) => ({
100
type: "loading" as const,
101
progress
102
}))
103
);
104
105
// Usage
106
const response1 = handleApiResponse({ status: "success", data: { users: [] } });
107
// { type: "success", payload: { users: [] } }
108
109
const response2 = handleApiResponse({ status: "error", message: "Not found", code: 404 });
110
// { type: "error", error: "404: Not found" }
111
```
112
113
### Advanced Pattern Matching
114
115
```typescript
116
import { match, when, Union, Object, String, Number, Array } from "runtypes";
117
118
// Complex data processing
119
const DataProcessor = match(
120
// Process strings
121
when(String, str => ({
122
type: "text",
123
length: str.length,
124
words: str.split(" ").length,
125
uppercase: str.toUpperCase()
126
})),
127
128
// Process numbers
129
when(Number, num => ({
130
type: "numeric",
131
value: num,
132
squared: num ** 2,
133
isEven: num % 2 === 0
134
})),
135
136
// Process arrays
137
when(Array(Unknown), arr => ({
138
type: "list",
139
count: arr.length,
140
isEmpty: arr.length === 0,
141
first: arr[0],
142
last: arr[arr.length - 1]
143
})),
144
145
// Process objects
146
when(Object({ name: String, age: Number }), person => ({
147
type: "person",
148
greeting: `Hello, ${person.name}!`,
149
isAdult: person.age >= 18,
150
category: person.age < 13 ? "child" : person.age < 20 ? "teen" : "adult"
151
}))
152
);
153
154
// Usage examples
155
console.log(DataProcessor("Hello World"));
156
// { type: "text", length: 11, words: 2, uppercase: "HELLO WORLD" }
157
158
console.log(DataProcessor(42));
159
// { type: "numeric", value: 42, squared: 1764, isEven: true }
160
161
console.log(DataProcessor([1, 2, 3]));
162
// { type: "list", count: 3, isEmpty: false, first: 1, last: 3 }
163
164
console.log(DataProcessor({ name: "Alice", age: 25 }));
165
// { type: "person", greeting: "Hello, Alice!", isAdult: true, category: "adult" }
166
```
167
168
## Built-in Union Pattern Matching
169
170
Union types also provide their own pattern matching method.
171
172
```typescript
173
import { Union, Object, Literal, String, Number } from "runtypes";
174
175
const Result = Union(
176
Object({ type: Literal("ok"), value: String }),
177
Object({ type: Literal("error"), message: String })
178
);
179
180
// Using Union's built-in match method
181
const processResult = Result.match(
182
// Case for "ok" variant
183
({ value }) => `Success: ${value}`,
184
// Case for "error" variant
185
({ message }) => `Failed: ${message}`
186
);
187
188
const result1 = processResult({ type: "ok", value: "Hello" }); // "Success: Hello"
189
const result2 = processResult({ type: "error", message: "Oops" }); // "Failed: Oops"
190
```
191
192
## Additional Utilities
193
194
### Optional Helper
195
196
While Optional is primarily used in object definitions, it can be useful for creating optional validation chains.
197
198
```typescript
199
import { Optional, String, Number } from "runtypes";
200
201
// Create optional validators
202
const OptionalString = Optional(String);
203
const OptionalNumber = Optional(Number, 0); // with default
204
205
// Check for optional presence
206
function processOptionalValue(value: unknown) {
207
if (Optional.isOptional(OptionalString)) {
208
console.log("This is an optional type");
209
}
210
211
// Optional validation allows undefined
212
const result = OptionalString.underlying.guard(value);
213
return result;
214
}
215
```
216
217
### Lazy Evaluation
218
219
For recursive type definitions and forward references.
220
221
```typescript
222
import { Lazy, Object, String, Number, Array, Union } from "runtypes";
223
224
// Recursive data structures
225
type TreeNodeType = {
226
value: string;
227
children: TreeNodeType[];
228
};
229
230
const TreeNode: Runtype<TreeNodeType> = Lazy(() => Object({
231
value: String,
232
children: Array(TreeNode) // Self-reference
233
}));
234
235
// Usage
236
const tree = TreeNode.check({
237
value: "root",
238
children: [
239
{
240
value: "child1",
241
children: [
242
{ value: "grandchild1", children: [] },
243
{ value: "grandchild2", children: [] }
244
]
245
},
246
{ value: "child2", children: [] }
247
]
248
});
249
250
// Mutually recursive types
251
type PersonType = {
252
name: string;
253
company?: CompanyType;
254
};
255
256
type CompanyType = {
257
name: string;
258
employees: PersonType[];
259
};
260
261
const Person: Runtype<PersonType> = Lazy(() => Object({
262
name: String,
263
company: Company.optional()
264
}));
265
266
const Company: Runtype<CompanyType> = Lazy(() => Object({
267
name: String,
268
employees: Array(Person)
269
}));
270
```
271
272
### InstanceOf Utility
273
274
Validate instanceof relationships with constructor functions.
275
276
```typescript
277
import { InstanceOf, Object, String, Union } from "runtypes";
278
279
// Built-in constructors
280
const DateValidator = InstanceOf(Date);
281
const RegExpValidator = InstanceOf(RegExp);
282
const ErrorValidator = InstanceOf(Error);
283
284
const date = DateValidator.check(new Date()); // Date
285
const regex = RegExpValidator.check(/pattern/); // RegExp
286
287
// Custom classes
288
class User {
289
constructor(public name: string, public age: number) {}
290
}
291
292
class Admin extends User {
293
constructor(name: string, age: number, public permissions: string[]) {
294
super(name, age);
295
}
296
}
297
298
const UserValidator = InstanceOf(User);
299
const AdminValidator = InstanceOf(Admin);
300
301
const user = new User("Alice", 25);
302
const admin = new Admin("Bob", 30, ["read", "write"]);
303
304
UserValidator.check(user); // ✓ User instance
305
UserValidator.check(admin); // ✓ Admin extends User
306
AdminValidator.check(admin); // ✓ Admin instance
307
AdminValidator.check(user); // ✗ throws ValidationError
308
309
// Combined with other validators
310
const UserData = Union(
311
InstanceOf(User),
312
Object({ name: String, age: Number }) // Plain object alternative
313
);
314
```
315
316
### Function Validation
317
318
Basic function validation (note: cannot validate function signatures at runtime).
319
320
```typescript
321
import { Function as FunctionValidator } from "runtypes";
322
323
// Validate that something is a function
324
const fn = FunctionValidator.check(() => "hello"); // Function
325
const method = FunctionValidator.check(Math.max); // Function
326
327
// Combined with other types
328
const Callback = Union(FunctionValidator, Literal(null));
329
330
function processWithCallback(data: unknown, callback: unknown) {
331
const validCallback = Callback.check(callback);
332
333
if (typeof validCallback === "function") {
334
return validCallback(data);
335
}
336
337
return data; // No callback provided
338
}
339
```
340
341
### Spread Utility
342
343
Enable rest/spread syntax in tuple definitions.
344
345
```typescript
346
import { Spread, Tuple, Array, String, Number } from "runtypes";
347
348
// Tuple with rest elements
349
const LogEntry = Tuple(
350
String, // timestamp
351
String, // level
352
Spread(Array(String)) // variable message parts
353
);
354
355
const entry1 = LogEntry.check(["2024-01-15T10:30:00Z", "INFO", "User", "logged", "in"]);
356
const entry2 = LogEntry.check(["2024-01-15T10:31:00Z", "ERROR", "Database", "connection", "failed", "retry", "in", "5s"]);
357
358
// Mixed tuple with leading, rest, and trailing
359
const DataRecord = Tuple(
360
Number, // id
361
String, // name
362
Spread(Array(Number)), // scores (variable length)
363
Boolean // active
364
);
365
366
const record = DataRecord.check([1, "Alice", 95, 87, 92, 88, true]);
367
// id=1, name="Alice", scores=[95,87,92,88], active=true
368
```
369
370
## Utility Composition
371
372
```typescript
373
import { match, when, Union, Object, Literal, String, Number, Array } from "runtypes";
374
375
// Complex validation and processing pipeline
376
const DataInput = Union(
377
String,
378
Number,
379
Array(String),
380
Object({ type: Literal("user"), name: String, age: Number }),
381
Object({ type: Literal("product"), name: String, price: Number })
382
);
383
384
const processInput = match(
385
when(String, str => ({ kind: "text", processed: str.trim().toLowerCase() })),
386
when(Number, num => ({ kind: "numeric", processed: num.toString() })),
387
when(Array(String), arr => ({ kind: "list", processed: arr.join(", ") })),
388
when(Object({ type: Literal("user"), name: String, age: Number }),
389
user => ({ kind: "user", processed: `${user.name} (${user.age})` })),
390
when(Object({ type: Literal("product"), name: String, price: Number }),
391
product => ({ kind: "product", processed: `${product.name}: $${product.price}` }))
392
);
393
394
// Usage
395
const results = [
396
processInput(" Hello World "),
397
processInput(42),
398
processInput(["apple", "banana", "cherry"]),
399
processInput({ type: "user", name: "Alice", age: 25 }),
400
processInput({ type: "product", name: "Widget", price: 19.99 })
401
];
402
403
results.forEach(result => {
404
console.log(`${result.kind}: ${result.processed}`);
405
});
406
// text: hello world
407
// numeric: 42
408
// list: apple, banana, cherry
409
// user: Alice (25)
410
// product: Widget: $19.99
411
```