3.4 KiB
3.4 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use intersection() for Type Combinations | MEDIUM | Manual field combination loses type relationships; intersection creates proper TypeScript intersection types | 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):
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):
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:
// 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:
// .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