Capítulo 49 de 61

Chapter 49: For Library Authors — Accepting User-Defined Schemas

Core Idea

A generic function that accepts "any Zod schema" should constrain its type parameter to extends z4.$ZodType (not use $ZodType<T> as a plain parameter type), so TypeScript preserves the caller's specific schema subclass instead of erasing it.

Key Concepts

  • function f<T>(schema: z4.$ZodType<T>) is the wrong pattern: no matter what concrete schema is passed, TypeScript infers schema as the base $ZodType, losing subclass-specific methods (e.g. calling .min() on the result becomes impossible).
  • function f<T extends z4.$ZodType>(schema: T) preserves the subclass: T is inferred as the caller's actual schema type (ZodString, ZodObject, etc.).
  • Narrowing to a specific subclass: <T extends z4.$ZodObject> restricts the function to only accept object schemas.
  • Narrowing by inferred output type: <T extends z4.$ZodType<string>> restricts to schemas whose output is (assignable to) string, regardless of which concrete schema class produces it.
  • Parsing inside library code: z4.$ZodType instances have no parsing methods of their own (those are added by Zod Classic/Mini) — use the top-level z4.parse()/z4.safeParse()/z4.parseAsync()/z4.safeParseAsync() functions instead.

Code Examples

import * as z4 from "zod/v4/core";

// WRONG: erases the caller's specific schema type
function inferSchemaWrong<T>(schema: z4.$ZodType<T>) { return schema; }
inferSchemaWrong(z.string()); // => $ZodType<string>, not ZodString

// RIGHT: preserves the specific schema subclass
function inferSchema<T extends z4.$ZodType>(schema: T) { return schema; }
inferSchema(z.string()); // => ZodString

// constrain to a subclass
function acceptObject<T extends z4.$ZodObject>(schema: T) { return schema; }

// constrain by output type instead of subclass
function acceptStringLike<T extends z4.$ZodType<string>>(schema: T) { return schema; }
acceptStringLike(z.string()); // ok
acceptStringLike(z.number()); // compile error — output isn't string

// parsing via the core-level functions, not schema methods
function parseData<T extends z4.$ZodType>(data: unknown, schema: T): z4.output<T> {
  return z4.parse(schema, data);
}
  • What it demonstrates: the correct generic-constraint pattern, narrowing by subclass vs. by output type, and parsing through core-level functions rather than instance methods.

Anti-patterns

  • Typing an accept-a-schema parameter as z4.$ZodType<T> directly: silently discards the caller's specific schema class, breaking downstream method access even though the code compiles.

Key Takeaways

  1. Always write <T extends z4.$ZodType>(schema: T), never (schema: z4.$ZodType<T>), when a library function needs to preserve the caller's exact schema type.
  2. Narrow with z4.$ZodObject (or another concrete subclass) when the function only makes sense for a specific schema shape; narrow with z4.$ZodType<OutputType> when only the inferred output type matters.
  3. z4.parse()/z4.safeParse() (top-level functions) are how library code parses through the core package — instance .parse() methods belong to Zod Classic/Mini, not core.

Connects To

  • For Library Authors — Peer Dependencies & Subpaths: the broader guidance this generic-typing pattern supports.
  • Basic Usage — Define, Parse, Handle Errors, Infer: the application-level .parse()/.safeParse() methods this chapter's core-level functions parallel.