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

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