Files
gruperly/.agents/skills/zod/references/compose-intersection.md
2026-09-04 16:49:24 -03:00

145 lines
3.4 KiB
Markdown

---
title: Use intersection() for Type Combinations
impact: MEDIUM
impactDescription: Manual field combination loses type relationships; intersection creates proper TypeScript intersection types
tags: compose, intersection, and, combination
---
## Use intersection() for Type Combinations
When you need an object that satisfies multiple schemas simultaneously (like combining a base type with mixins), use `.and()` or `z.intersection()`. This creates proper TypeScript intersection types and validates against all schemas.
**Incorrect (manual combination):**
```typescript
import { z } from 'zod'
const timestampsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
})
const softDeleteSchema = z.object({
deletedAt: z.date().nullable(),
deletedBy: z.string().nullable(),
})
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
// Manual combination - verbose and error-prone
const fullUserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
createdAt: z.date(),
updatedAt: z.date(),
deletedAt: z.date().nullable(),
deletedBy: z.string().nullable(),
})
```
**Correct (using intersection):**
```typescript
import { z } from 'zod'
const timestampsSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
})
const softDeleteSchema = z.object({
deletedAt: z.date().nullable(),
deletedBy: z.string().nullable(),
})
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
// Using .and() for intersection
const fullUserSchema = userSchema
.and(timestampsSchema)
.and(softDeleteSchema)
// Or using z.intersection()
const fullUserSchema2 = z.intersection(
z.intersection(userSchema, timestampsSchema),
softDeleteSchema
)
type FullUser = z.infer<typeof fullUserSchema>
// {
// id: string;
// name: string;
// email: string;
// createdAt: Date;
// updatedAt: Date;
// deletedAt: Date | null;
// deletedBy: string | null;
// }
```
**Creating mixins:**
```typescript
// Reusable mixins
const auditable = z.object({
createdBy: z.string(),
updatedBy: z.string(),
})
const versioned = z.object({
version: z.number().int().positive(),
})
const tagged = z.object({
tags: z.array(z.string()),
})
// Apply mixins to any schema
function withAudit<T extends z.ZodRawShape>(schema: z.ZodObject<T>) {
return schema.and(auditable).and(timestampsSchema)
}
function withVersioning<T extends z.ZodRawShape>(schema: z.ZodObject<T>) {
return schema.and(versioned)
}
// Usage
const documentSchema = z.object({
id: z.string(),
title: z.string(),
content: z.string(),
})
const fullDocumentSchema = withAudit(withVersioning(documentSchema))
```
**Intersection vs Merge:**
```typescript
// .merge() - replaces fields from first with second
const a = z.object({ x: z.string(), y: z.number() })
const b = z.object({ y: z.string() }) // y is string, not number
a.merge(b) // { x: string, y: string } - b's y wins
// .and() - requires fields to be compatible
// If both have y with different types, intersection fails at runtime
a.and(b) // Validation will fail - y can't be both number and string
```
**When NOT to use this pattern:**
- When schemas have overlapping fields with different types (use merge)
- When you need to override fields (use extend)
- Simple cases where extend works fine
Reference: [Zod API - intersection](https://zod.dev/api#intersection)