0
# Union and Intersection Types
1
2
Combine schemas using union (OR) or intersection (AND) logic.
3
4
## Union
5
6
```typescript { .api }
7
function union<T extends readonly [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]>(schemas: T): ZodUnion<T>;
8
```
9
10
**Examples:**
11
```typescript
12
// Basic union
13
z.union([z.string(), z.number()])
14
// type: string | number
15
16
// Multiple types
17
z.union([z.string(), z.number(), z.boolean()])
18
19
// Complex unions
20
z.union([
21
z.object({ type: z.literal("user"), name: z.string() }),
22
z.object({ type: z.literal("admin"), permissions: z.array(z.string()) }),
23
])
24
25
// Nullable/optional patterns
26
z.union([z.string(), z.null()]) // string | null
27
z.union([z.string(), z.undefined()]) // string | undefined
28
// Better: use .nullable() or .optional()
29
```
30
31
## Discriminated Union
32
33
Optimized union validation using a discriminator field.
34
35
```typescript { .api }
36
function discriminatedUnion<
37
Discriminator extends string,
38
Options extends ZodDiscriminatedUnionOption<Discriminator>[]
39
>(discriminator: Discriminator, options: Options): ZodDiscriminatedUnion<Discriminator, Options>;
40
```
41
42
**Examples:**
43
```typescript
44
// Basic discriminated union
45
const MessageSchema = z.discriminatedUnion("type", [
46
z.object({ type: z.literal("text"), content: z.string() }),
47
z.object({ type: z.literal("image"), url: z.string().url() }),
48
z.object({ type: z.literal("video"), url: z.string().url(), duration: z.number() }),
49
]);
50
51
type Message = z.infer<typeof MessageSchema>;
52
// { type: "text"; content: string }
53
// | { type: "image"; url: string }
54
// | { type: "video"; url: string; duration: number }
55
56
// API response types
57
const ResponseSchema = z.discriminatedUnion("status", [
58
z.object({ status: z.literal("success"), data: z.any() }),
59
z.object({ status: z.literal("error"), error: z.string() }),
60
z.object({ status: z.literal("loading") }),
61
]);
62
63
// Form state
64
const FormStateSchema = z.discriminatedUnion("state", [
65
z.object({ state: z.literal("idle") }),
66
z.object({ state: z.literal("submitting") }),
67
z.object({ state: z.literal("success"), data: z.any() }),
68
z.object({ state: z.literal("error"), error: z.string() }),
69
]);
70
```
71
72
## Intersection
73
74
Combines two schemas (AND logic).
75
76
```typescript { .api }
77
function intersection<A extends ZodTypeAny, B extends ZodTypeAny>(
78
left: A,
79
right: B
80
): ZodIntersection<A, B>;
81
82
// Or use method
83
schemaA.and(schemaB);
84
```
85
86
**Examples:**
87
```typescript
88
// Basic intersection
89
const BaseUser = z.object({ id: z.string(), name: z.string() });
90
const WithEmail = z.object({ email: z.string().email() });
91
92
z.intersection(BaseUser, WithEmail)
93
// Or: BaseUser.and(WithEmail)
94
// type: { id: string; name: string; email: string }
95
96
// Multiple intersections
97
const WithTimestamps = z.object({
98
createdAt: z.date(),
99
updatedAt: z.date(),
100
});
101
102
BaseUser.and(WithEmail).and(WithTimestamps)
103
104
// Note: For objects, .merge() is usually better
105
BaseUser.merge(WithEmail).merge(WithTimestamps)
106
```
107
108
## Common Patterns
109
110
```typescript
111
// Nullable pattern
112
const NullableString = z.union([z.string(), z.null()]);
113
// Better: z.string().nullable()
114
115
// Optional pattern
116
const OptionalString = z.union([z.string(), z.undefined()]);
117
// Better: z.string().optional()
118
119
// API response with success/error
120
const APIResponse = z.discriminatedUnion("success", [
121
z.object({
122
success: z.literal(true),
123
data: z.any(),
124
}),
125
z.object({
126
success: z.literal(false),
127
error: z.string(),
128
code: z.number(),
129
}),
130
]);
131
132
// Event types
133
const EventSchema = z.discriminatedUnion("event", [
134
z.object({ event: z.literal("click"), x: z.number(), y: z.number() }),
135
z.object({ event: z.literal("scroll"), scrollY: z.number() }),
136
z.object({ event: z.literal("resize"), width: z.number(), height: z.number() }),
137
]);
138
139
// Payment methods
140
const PaymentSchema = z.discriminatedUnion("method", [
141
z.object({
142
method: z.literal("card"),
143
cardNumber: z.string(),
144
cvv: z.string(),
145
}),
146
z.object({
147
method: z.literal("paypal"),
148
email: z.string().email(),
149
}),
150
z.object({
151
method: z.literal("crypto"),
152
wallet: z.string(),
153
currency: z.enum(["BTC", "ETH"]),
154
}),
155
]);
156
157
// Composing with intersection
158
const BaseEntity = z.object({ id: z.string() });
159
const Timestamped = z.object({ createdAt: z.date(), updatedAt: z.date() });
160
const Audited = z.object({ createdBy: z.string(), updatedBy: z.string() });
161
162
const FullEntity = BaseEntity.and(Timestamped).and(Audited);
163
// Or use merge for objects
164
const FullEntityMerged = BaseEntity.merge(Timestamped).merge(Audited);
165
```
166
167
## Type Inference
168
169
```typescript
170
const UnionSchema = z.union([z.string(), z.number()]);
171
type UnionType = z.infer<typeof UnionSchema>; // string | number
172
173
const DiscriminatedSchema = z.discriminatedUnion("type", [
174
z.object({ type: z.literal("a"), value: z.string() }),
175
z.object({ type: z.literal("b"), value: z.number() }),
176
]);
177
type DiscriminatedType = z.infer<typeof DiscriminatedSchema>;
178
// { type: "a"; value: string } | { type: "b"; value: number }
179
180
const IntersectionSchema = z.intersection(
181
z.object({ a: z.string() }),
182
z.object({ b: z.number() })
183
);
184
type IntersectionType = z.infer<typeof IntersectionSchema>;
185
// { a: string; b: number }
186
```
187
188
## When to Use
189
190
- **Union**: Multiple valid types (OR logic)
191
- **Discriminated Union**: Tagged union types with better performance
192
- **Intersection**: Combine types (AND logic), but prefer `.merge()` for objects
193