0
# Union and Intersection Types
1
2
Combines multiple runtypes to create flexible validation schemas. Union types validate values that match any of several alternatives, while intersection types require values to match all specified types.
3
4
## Capabilities
5
6
### Union
7
8
Validates values that match at least one of the provided alternative runtypes.
9
10
```typescript { .api }
11
/**
12
* Creates a validator that accepts values matching any of the provided alternatives
13
* @param alternatives - Array of runtypes, value must match at least one
14
* @example Union(String, Number).check("hello") // "hello"
15
* @example Union(String, Number).check(42) // 42
16
* @example Union(String, Number).check(true) // throws ValidationError
17
*/
18
function Union<T extends readonly Runtype[]>(...alternatives: T): UnionRuntype<T>;
19
20
interface UnionRuntype<T> extends Runtype<UnionType<T>> {
21
tag: "union";
22
alternatives: T;
23
match<C extends Cases<T>>(...cases: C): Matcher<T, ReturnType<C[number]>>;
24
}
25
```
26
27
**Usage Examples:**
28
29
```typescript
30
import { Union, String, Number, Boolean, Literal, Object } from "runtypes";
31
32
// Basic union types
33
const StringOrNumber = Union(String, Number);
34
const value1 = StringOrNumber.check("hello"); // "hello"
35
const value2 = StringOrNumber.check(42); // 42
36
37
// Multiple alternatives
38
const ID = Union(String, Number, BigInt);
39
const userId = ID.check("user_123"); // "user_123"
40
const postId = ID.check(456); // 456
41
42
// Literal unions (enum-like)
43
const Status = Union(
44
Literal("pending"),
45
Literal("approved"),
46
Literal("rejected"),
47
Literal("cancelled")
48
);
49
50
type StatusType = Static<typeof Status>; // "pending" | "approved" | "rejected" | "cancelled"
51
52
// Nullable types using union
53
const OptionalString = Union(String, Literal(null));
54
const OptionalNumber = Union(Number, Literal(undefined));
55
56
// Or use built-in helpers
57
const NullableString = String.nullable(); // Union(String, Literal(null))
58
const UndefinedableNumber = Number.undefinedable(); // Union(Number, Literal(undefined))
59
```
60
61
### Discriminated Unions
62
63
Create type-safe discriminated unions for complex data structures.
64
65
```typescript
66
import { Union, Object, Literal, String, Number, Array } from "runtypes";
67
68
// Shape discriminated union
69
const Circle = Object({
70
type: Literal("circle"),
71
radius: Number,
72
center: Object({ x: Number, y: Number })
73
});
74
75
const Rectangle = Object({
76
type: Literal("rectangle"),
77
width: Number,
78
height: Number,
79
topLeft: Object({ x: Number, y: Number })
80
});
81
82
const Triangle = Object({
83
type: Literal("triangle"),
84
vertices: Array(Object({ x: Number, y: Number }))
85
});
86
87
const Shape = Union(Circle, Rectangle, Triangle);
88
89
type ShapeType = Static<typeof Shape>;
90
// { type: "circle", radius: number, center: { x: number, y: number } } |
91
// { type: "rectangle", width: number, height: number, topLeft: { x: number, y: number } } |
92
// { type: "triangle", vertices: { x: number, y: number }[] }
93
94
// Usage with type narrowing
95
function calculateArea(shape: unknown): number {
96
const validShape = Shape.check(shape);
97
98
switch (validShape.type) {
99
case "circle":
100
return Math.PI * validShape.radius ** 2;
101
case "rectangle":
102
return validShape.width * validShape.height;
103
case "triangle":
104
// Complex triangle area calculation
105
return 0; // simplified
106
}
107
}
108
```
109
110
### Pattern Matching with Union
111
112
Use the built-in pattern matching functionality for unions.
113
114
```typescript
115
import { Union, match, when, Literal, Object, String, Number } from "runtypes";
116
117
const Result = Union(
118
Object({ type: Literal("success"), data: String }),
119
Object({ type: Literal("error"), message: String, code: Number }),
120
Object({ type: Literal("loading") })
121
);
122
123
// Pattern matching
124
const handleResult = match(
125
when(Object({ type: Literal("success"), data: String }),
126
({ data }) => `Success: ${data}`),
127
when(Object({ type: Literal("error"), message: String, code: Number }),
128
({ message, code }) => `Error ${code}: ${message}`),
129
when(Object({ type: Literal("loading") }),
130
() => "Loading...")
131
);
132
133
// Usage
134
const response = handleResult({ type: "success", data: "Hello World" });
135
// "Success: Hello World"
136
```
137
138
### Intersect
139
140
Validates values that match all of the provided runtypes simultaneously.
141
142
```typescript { .api }
143
/**
144
* Creates a validator that requires values to match all provided runtypes
145
* @param intersectees - Array of runtypes, value must match all of them
146
* @example Intersect(Object({name: String}), Object({age: Number})).check({name: "Alice", age: 25})
147
*/
148
function Intersect<T extends readonly Runtype[]>(...intersectees: T): IntersectRuntype<T>;
149
150
interface IntersectRuntype<T> extends Runtype<IntersectType<T>> {
151
tag: "intersect";
152
intersectees: T;
153
}
154
```
155
156
**Usage Examples:**
157
158
```typescript
159
import { Intersect, Object, String, Number, Boolean } from "runtypes";
160
161
// Combining object types
162
const PersonBase = Object({
163
name: String,
164
age: Number
165
});
166
167
const ContactInfo = Object({
168
email: String,
169
phone: String.optional()
170
});
171
172
const PersonWithContact = Intersect(PersonBase, ContactInfo);
173
174
type PersonWithContactType = Static<typeof PersonWithContact>;
175
// {
176
// name: string;
177
// age: number;
178
// email: string;
179
// phone?: string;
180
// }
181
182
const person = PersonWithContact.check({
183
name: "Alice",
184
age: 25,
185
email: "alice@example.com",
186
phone: "555-0123"
187
});
188
```
189
190
### Mixin Patterns with Intersect
191
192
```typescript
193
import { Intersect, Object, String, Number, Boolean } from "runtypes";
194
195
// Base entity
196
const BaseEntity = Object({
197
id: String,
198
createdAt: Number,
199
updatedAt: Number
200
});
201
202
// Audit trail
203
const Auditable = Object({
204
createdBy: String,
205
updatedBy: String,
206
version: Number
207
});
208
209
// Soft delete capability
210
const SoftDeletable = Object({
211
deleted: Boolean,
212
deletedAt: Number.optional(),
213
deletedBy: String.optional()
214
});
215
216
// User entity with mixins
217
const User = Intersect(
218
BaseEntity,
219
Auditable,
220
SoftDeletable,
221
Object({
222
name: String,
223
email: String,
224
role: Union(Literal("admin"), Literal("user"))
225
})
226
);
227
228
type UserType = Static<typeof User>;
229
// Combines all properties from all intersected types
230
```
231
232
## Advanced Union Patterns
233
234
### Async Result Types
235
236
```typescript
237
import { Union, Object, Literal, String, Unknown } from "runtypes";
238
239
const AsyncResult = Union(
240
Object({
241
status: Literal("pending")
242
}),
243
Object({
244
status: Literal("fulfilled"),
245
value: Unknown
246
}),
247
Object({
248
status: Literal("rejected"),
249
reason: String
250
})
251
);
252
253
// Usage in async contexts
254
async function handleAsyncResult(result: unknown) {
255
const validResult = AsyncResult.check(result);
256
257
if (validResult.status === "fulfilled") {
258
console.log("Success:", validResult.value);
259
} else if (validResult.status === "rejected") {
260
console.error("Error:", validResult.reason);
261
} else {
262
console.log("Still pending...");
263
}
264
}
265
```
266
267
### Conditional Types with Union
268
269
```typescript
270
import { Union, Object, Literal, String, Number, Array } from "runtypes";
271
272
// API endpoint responses
273
const UserResponse = Object({
274
type: Literal("user"),
275
data: Object({
276
id: Number,
277
name: String,
278
email: String
279
})
280
});
281
282
const PostResponse = Object({
283
type: Literal("post"),
284
data: Object({
285
id: Number,
286
title: String,
287
content: String,
288
authorId: Number
289
})
290
});
291
292
const ListResponse = Object({
293
type: Literal("list"),
294
data: Array(Unknown),
295
pagination: Object({
296
page: Number,
297
total: Number,
298
hasMore: Boolean
299
})
300
});
301
302
const ApiResponse = Union(UserResponse, PostResponse, ListResponse);
303
304
// Type-safe response handling
305
function processResponse(response: unknown) {
306
const validResponse = ApiResponse.check(response);
307
308
switch (validResponse.type) {
309
case "user":
310
// TypeScript knows data is user object
311
return `User: ${validResponse.data.name}`;
312
case "post":
313
// TypeScript knows data is post object
314
return `Post: ${validResponse.data.title}`;
315
case "list":
316
// TypeScript knows about pagination
317
return `List: ${validResponse.data.length} items (page ${validResponse.pagination.page})`;
318
}
319
}
320
```
321
322
## Combining Union and Intersection
323
324
```typescript
325
import { Union, Intersect, Object, Literal, String, Number } from "runtypes";
326
327
// Base types
328
const Identifiable = Object({ id: String });
329
const Timestamped = Object({ timestamp: Number });
330
331
// Different entity types
332
const User = Object({ type: Literal("user"), name: String, email: String });
333
const Post = Object({ type: Literal("post"), title: String, content: String });
334
335
// Combine with mixins using intersection, then union for alternatives
336
const TimestampedUser = Intersect(User, Identifiable, Timestamped);
337
const TimestampedPost = Intersect(Post, Identifiable, Timestamped);
338
339
const Entity = Union(TimestampedUser, TimestampedPost);
340
341
type EntityType = Static<typeof Entity>;
342
// ({ type: "user", name: string, email: string } & { id: string } & { timestamp: number }) |
343
// ({ type: "post", title: string, content: string } & { id: string } & { timestamp: number })
344
```