Capítulo 16 de 61

Chapter 16: Objects — Extending & Deriving

Core Idea

Zod provides TypeScript-utility-type-inspired methods (.extend(), .pick(), .omit(), .partial(), .required()) for deriving new object schemas from an existing one, plus spread syntax as a tsc-cheaper alternative to .extend().

Key Concepts

  • .extend({...}): adds fields to a schema; if a key already exists, the new field overrides it. Throws when used on a schema carrying refinements.
  • Spread syntax alternative: z.object({ ...Base.shape, newField: z.string() }) achieves the same result using plain JS, works identically in Zod and Zod Mini, and avoids the quadratic tsc cost of chained .extend() calls.
  • .safeExtend({...}): like .extend(), but rejects overriding a field with a non-assignable schema (type-checked at compile time), and — unlike .extend() — works on schemas with refinements, inheriting them.
  • .pick({...}) / .omit({...}): derive a schema with only the given keys, or all keys except the given ones (mirrors TS Pick/Omit).
  • .partial() / .partial({...}): makes all (or selected) fields optional (via .optional()).
  • .exactPartial(): like .partial() but wraps fields in .exactOptional() instead of .optional().
  • z.deepPartial(schema): recursively makes nested objects/arrays/tuples/unions/records partial too, not just the top level; a discriminated union degrades to a plain union since an optional discriminant breaks fast-path lookup; throws on schemas with their own refinement.
  • .required() / .required({...}): the inverse of .partial() — makes all or selected fields required.

Code Examples

const Recipe = z.object({
  title: z.string(),
  description: z.string().optional(),
  ingredients: z.array(z.string()),
});

const JustTheTitle = Recipe.pick({ title: true });
const RecipeNoDescription = Recipe.omit({ description: true });
const PartialRecipe = Recipe.partial();
const RecipeRequiredDescription = Recipe.required({ description: true });

// extending a schema that has a refinement — .extend() would throw, .safeExtend() works
const Base = z.object({ a: z.string(), b: z.string() })
  .refine(user => user.a === user.b);
const Extended = Base.safeExtend({ a: z.string().min(10) }); // inherits the refinement
  • What it demonstrates: pick/omit/partial/required for reshaping schemas, and .safeExtend() as the refinement-safe alternative to .extend().

Anti-patterns

  • Chaining many .extend() calls on large schemas: each call is tsc-expensive and the cost compounds quadratically due to a TypeScript limitation — prefer spread syntax ({ ...A.shape, ...B.shape }) for merging multiple schemas.
  • Calling .extend() on a schema with a .refine(): throws; use .safeExtend() instead, which both allows it and preserves the refinement.

Key Takeaways

  1. Prefer object-spread over .extend() for merging schemas — it's language-native, works the same in Zod and Zod Mini, and avoids compile-time cost blowup on chains.
  2. .safeExtend() is the type-safe, refinement-preserving version of .extend() — use it whenever the base schema might carry a .refine().
  3. z.deepPartial() recurses through the whole schema tree, unlike .partial(), but changes the semantics of discriminated unions (they lose their fast-path discriminant).

Connects To

  • Objects — Definition, Strictness & Shape: the base object APIs these methods derive from.
  • Refinements: why .extend() throws on refined schemas and .safeExtend() doesn't.