Files
gruperly/.agents/skills/zod/references/object-extend-for-composition.md
2026-09-04 16:49:24 -03:00

3.4 KiB

title, impact, impactDescription, tags
title impact impactDescription tags
Use extend() for Adding Fields MEDIUM-HIGH Merging objects manually loses type information; extend() preserves types and allows overriding fields safely object, extend, composition, inheritance

Use extend() for Adding Fields

When building on existing schemas, use .extend() to add new fields rather than manually spreading. Extend preserves type information, allows overriding existing fields, and keeps the schema relationship explicit.

Incorrect (manual object spreading):

import { z } from 'zod'

const baseUserSchema = z.object({
  id: z.string(),
  name: z.string(),
})

// Manual spreading loses Zod's schema relationship
const adminUserSchema = z.object({
  ...baseUserSchema.shape,  // Accessing internal .shape
  role: z.literal('admin'),
  permissions: z.array(z.string()),
})

// Problems:
// 1. If baseUserSchema changes, TypeScript might not catch issues
// 2. Can't override fields easily
// 3. Loses schema methods and metadata

Correct (using extend):

import { z } from 'zod'

const baseUserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
})

// Extend to add fields
const adminUserSchema = baseUserSchema.extend({
  role: z.literal('admin'),
  permissions: z.array(z.string()),
})

type AdminUser = z.infer<typeof adminUserSchema>
// {
//   id: string;
//   name: string;
//   email: string;
//   role: 'admin';
//   permissions: string[];
// }

// Override existing fields
const strictEmailSchema = baseUserSchema.extend({
  email: z.string().email().endsWith('@company.com'),  // Stricter validation
})

Building hierarchies with extend:

// Base entity with common fields
const entitySchema = z.object({
  id: z.string().uuid(),
  createdAt: z.date(),
  updatedAt: z.date(),
})

// User extends entity
const userSchema = entitySchema.extend({
  email: z.string().email(),
  name: z.string(),
})

// Product extends entity
const productSchema = entitySchema.extend({
  name: z.string(),
  price: z.number().positive(),
  sku: z.string(),
})

// Order extends entity with references
const orderSchema = entitySchema.extend({
  userId: z.string().uuid(),
  items: z.array(z.object({
    productId: z.string().uuid(),
    quantity: z.number().int().positive(),
  })),
  total: z.number().positive(),
})

Combining extend with other methods:

const baseSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
})

// Create input: no id, add password
const createSchema = baseSchema
  .omit({ id: true })
  .extend({
    password: z.string().min(8),
  })

// Update input: all optional except id
const updateSchema = baseSchema
  .partial()
  .extend({
    id: z.string(),  // Override to make required
  })

Merge for combining independent schemas:

const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
})

const contactSchema = z.object({
  email: z.string().email(),
  phone: z.string(),
})

// Merge combines two schemas (both required)
const customerSchema = addressSchema.merge(contactSchema)
// { street: string; city: string; email: string; phone: string }

When NOT to use this pattern:

  • When schemas are genuinely independent (use merge or intersection)
  • When you need to remove fields (use omit)

Reference: Zod API - extend