Agentient

zod-schema-type-inference-chain

Zod v4 schema definition patterns with the Type Inference Chain for absolute type safety. PROACTIVELY activate for: (1) defining Zod schemas as single source of truth, (2) using z.infer for type generation, (3) implementing safe parsing with safeParse. Triggers: "zod", "schema", "z.infer"

Agentient 2 1 Updated 4mo ago
GitHub

Install

npx skillscat add agentient/vibekit/zod-schema-type-inference-chain

Install via the SkillsCat registry.

SKILL.md

Zod Schema Type Inference Chain

The Type Inference Chain Pattern (CRITICAL)

The Type Inference Chain is the foundational pattern for all state and form management:

Zod Schema (single source of truth)
    |
z.infer<typeof schema> (generate TypeScript type)
    |
useForm<Type>() (type-safe form)
    |
Zustand Store<Type> (type-safe state)

This pattern ensures absolute type safety across your entire data layer by maintaining one single source of truth: the Zod schema.

Why This Pattern is Critical

Problem Without Type Inference Chain:

// BAD: Multiple sources of truth
interface UserData {           // Manual type definition
  email: string;
  age: number;
}

const userSchema = z.object({  // Zod schema
  email: z.string().email(),
  age: z.number().min(18),
});

// These can drift out of sync!

Solution With Type Inference Chain:

// GOOD: Single source of truth
const userSchema = z.object({
  email: z.string().email('Invalid email'),
  age: z.number().min(18, 'Must be 18+'),
});

// Type is automatically inferred from schema
type UserData = z.infer<typeof userSchema>;

// Now the schema and type are ALWAYS in sync!

Schema Definition Patterns

Basic Object Schema

import { z } from 'zod';

const userSchema = z.object({
  // String with validations
  username: z.string()
    .min(3, 'Username must be at least 3 characters')
    .max(20, 'Username must be at most 20 characters')
    .regex(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores'),

  // Email validation
  email: z.string().email('Invalid email address'),

  // Number with range
  age: z.number()
    .int('Must be a whole number')
    .min(18, 'Must be 18 or older')
    .max(120, 'Invalid age'),

  // Optional field
  middleName: z.string().optional(),

  // Nullable field
  nickname: z.string().nullable(),

  // Enum
  role: z.enum(['user', 'admin', 'moderator']),

  // Boolean
  isActive: z.boolean(),

  // Array
  tags: z.array(z.string())
    .min(1, 'At least one tag required')
    .max(5, 'Maximum 5 tags'),
});

// Infer TypeScript type
type User = z.infer<typeof userSchema>;

Nested Object Schema

const addressSchema = z.object({
  street: z.string().min(1, 'Street required'),
  city: z.string().min(1, 'City required'),
  state: z.string().length(2, 'Use 2-letter state code'),
  zipCode: z.string().regex(/^\d{5}$/, 'Must be 5 digits'),
});

const userWithAddressSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  address: addressSchema, // Nested object
});

type UserWithAddress = z.infer<typeof userWithAddressSchema>;

Schema Composition and Reusability

Using .extend()

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

// Add more fields to base schema
const fullUserSchema = baseUserSchema.extend({
  name: z.string(),
  age: z.number(),
  role: z.enum(['user', 'admin']),
});

type FullUser = z.infer<typeof fullUserSchema>;

Using .pick() and .omit()

const userSchema = z.object({
  id: z.string(),
  email: z.string().email(),
  password: z.string(),
  name: z.string(),
});

// Pick only specific fields
const loginSchema = userSchema.pick({
  email: true,
  password: true,
});
// { email: string; password: string; }

// Omit specific fields
const publicUserSchema = userSchema.omit({
  password: true,
});
// { id: string; email: string; name: string; }

Cross-Field Validation with .refine()

Password Confirmation

const passwordSchema = z.object({
  password: z.string()
    .min(8, 'Password must be at least 8 characters'),
  confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
  message: 'Passwords do not match',
  path: ['confirmPassword'], // Error appears on confirmPassword field
});

type PasswordForm = z.infer<typeof passwordSchema>;

Conditional Validation

const shippingSchema = z.object({
  shippingMethod: z.enum(['pickup', 'delivery']),
  address: z.string().optional(),
}).refine(
  (data) => {
    // If delivery is selected, address is required
    if (data.shippingMethod === 'delivery') {
      return data.address && data.address.length > 0;
    }
    return true;
  },
  {
    message: 'Address required for delivery',
    path: ['address'],
  }
);

Safe Error Handling with .safeParse()

Basic Safe Parsing

const userSchema = z.object({
  email: z.string().email(),
  age: z.number().min(18),
});

// Unsafe data from user input or API
const userData = {
  email: 'invalid-email',
  age: 15,
};

// Use safeParse for validation
const result = userSchema.safeParse(userData);

if (!result.success) {
  // Validation failed - handle errors
  const formatted = result.error.format();
  console.log(formatted.email?._errors); // ["Invalid email address"]
  console.log(formatted.age?._errors); // ["Must be 18 or older"]

  // Get flat errors
  const flat = result.error.flatten();
  console.log(flat.fieldErrors);
  // {
  //   email: ["Invalid email address"],
  //   age: ["Must be 18 or older"]
  // }
} else {
  // Validation succeeded - use validated data
  const validUser = result.data; // Fully typed!
}

Anti-Patterns (DO NOT DO)

Manually Defining Types

// WRONG: Manual type separate from schema
interface UserData {
  email: string;
  age: number;
}

const userSchema = z.object({
  email: z.string().email(),
  age: z.number(),
});

// Problem: These can drift out of sync!

Correct:

const userSchema = z.object({
  email: z.string().email(),
  age: z.number(),
});

type UserData = z.infer<typeof userSchema>; // Always in sync!

Using .parse() Without Try/Catch

// WRONG: Throws unhandled exception on invalid data
const user = userSchema.parse(untrustedData);

Correct:

const result = userSchema.safeParse(untrustedData);
if (!result.success) {
  // Handle error
} else {
  const user = result.data;
}

Summary

The zod-schema-type-inference-chain skill establishes the foundational pattern:

  1. Define Zod schema - Single source of truth for data structure and validation
  2. Infer TypeScript type - Use z.infer<typeof schema> to generate type
  3. Use in forms - Pass type to useForm<Type>({ resolver: zodResolver(schema) })
  4. Use in stores - Pass type to Zustand store interface
  5. Validate safely - Use .safeParse() for all external data

Related Skills: rhf-zod-schema-integration, zustand-v5-typed-store-creation