145 lines
3.4 KiB
Markdown
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)
|