--- 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 // { // 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(schema: z.ZodObject) { return schema.and(auditable).and(timestampsSchema) } function withVersioning(schema: z.ZodObject) { 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)