Zod 模式验证

Zod TypeScript优先的模式验证示例,包括原语、对象、数组、转换、自定义验证器和错误处理

💻 Zod 基础和原语类型 typescript

🟢 simple ⭐⭐

基础Zod模式验证,包括原始类型、基本验证和简单转换

⏱️ 25 min 🏷️ zod, validation, typescript, runtime
Prerequisites: TypeScript basics, JavaScript
// Zod Basics and Primitive Types Example

import { z } from 'zod';

// 1. Primitive Types
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
const undefinedSchema = z.undefined();
const nullSchema = z.null();

// Test primitive validation
console.log(stringSchema.parse('hello')); // ✅ 'hello'
console.log(numberSchema.parse(42)); // ✅ 42
console.log(booleanSchema.parse(true)); // ✅ true
console.log(dateSchema.parse(new Date())); // ✅ Date object
console.log(undefinedSchema.parse(undefined)); // ✅ undefined
console.log(nullSchema.parse(null)); // ✅ null

// 2. Type Constraints
const minStringLength = z.string().min(5);
const maxStringLength = z.string().max(10);
const emailRegex = z.string().email();
const urlRegex = z.string().url();

console.log(minStringLength.parse('hello world')); // ✅
console.log(maxStringLength.parse('short')); // ✅
console.log(emailRegex.parse('[email protected]')); // ✅
console.log(urlRegex.parse('https://example.com')); // ✅

// 3. Number Constraints
const positiveNumber = z.number().positive();
const nonNegativeNumber = z.number().nonnegative();
const maxNumber = z.number().max(100);
const minNumber = z.number().min(18);
const intNumber = z.number().int();

console.log(positiveNumber.parse(10)); // ✅
console.log(nonNegativeNumber.parse(0)); // ✅
console.log(maxNumber.parse(50)); // ✅
console.log(minNumber.parse(25)); // ✅
console.log(intNumber.parse(42)); // ✅

// 4. Optional and Nullable Types
const optionalString = z.string().optional();
const nullableString = z.string().nullable();
const optionalOrNullableString = z.string().optional().nullable();

console.log(optionalString.parse('hello')); // ✅ 'hello'
console.log(optionalString.parse(undefined)); // ✅ undefined
console.log(nullableString.parse(null)); // ✅ null
console.log(nullableString.parse('hello')); // ✅ 'hello'

// 5. Default Values
const stringWithDefault = z.string().default('default value');
const optionalStringWithDefault = z.string().optional().default('optional default');

console.log(stringWithDefault.parse(undefined)); // ✅ 'default value'
console.log(optionalStringWithDefault.parse(undefined)); // ✅ 'optional default'

// 6. Enum-like Values
const directionSchema = z.enum(['north', 'south', 'east', 'west']);
const brandSchema = z.enum(['Nike', 'Adidas', 'Puma', 'Reebok']);

console.log(directionSchema.parse('north')); // ✅ 'north'
console.log(brandSchema.parse('Nike')); // ✅ 'Nike'

// 7. Literal Types
const trueSchema = z.literal(true);
const numberLiteral = z.literal(42);
const stringLiteral = z.literal('specific-value');

console.log(trueSchema.parse(true)); // ✅ true
console.log(numberLiteral.parse(42)); // ✅ 42
console.log(stringLiteral.parse('specific-value')); // ✅ 'specific-value'

// 8. Union Types
const stringOrNumber = z.union([z.string(), z.number()]);
const colorSchema = z.union([z.literal('red'), z.literal('green'), z.literal('blue')]);

console.log(stringOrNumber.parse('hello')); // ✅ 'hello'
console.log(stringOrNumber.parse(42)); // ✅ 42
console.log(colorSchema.parse('red')); // ✅ 'red'

// 9. Intersection Types
const personName = z.object({
  firstName: z.string(),
  lastName: z.string(),
});

const personAge = z.object({
  age: z.number().positive(),
});

const personWithDetails = z.intersection(personName, personAge);

console.log(personWithDetails.parse({
  firstName: 'John',
  lastName: 'Doe',
  age: 30
})); // ✅

// 10. Safe Parsing
const result = stringSchema.safeParse(123);
if (result.success) {
  console.log('Valid:', result.data);
} else {
  console.log('Invalid:', result.error);
}

// 11. Custom Validation
const customEmailSchema = z.string().email().refine((email) => {
  return email.endsWith('@company.com');
}, {
  message: 'Email must be from @company.com domain',
  path: ['domain']
});

try {
  customEmailSchema.parse('[email protected]'); // ✅
  customEmailSchema.parse('[email protected]'); // ❌
} catch (error) {
  console.error('Custom validation error:', error);
}

// 12. Transformations
const stringToNumber = z.string().transform((str) => parseInt(str, 10));
const uppercaseString = z.string().transform((str) => str.toUpperCase());
const trimmedString = z.string().transform((str) => str.trim());

const transformedNumber = stringToNumber.parse('123'); // ✅ 123 (number)
const transformedUpper = uppercaseString.parse('hello'); // ✅ 'HELLO'
const transformedTrim = trimmedString.parse('  hello  '); // ✅ 'hello'

// 13. Preprocessing and Postprocessing
const preprocessSchema = z.string().transform((str) => str.trim().toLowerCase());

const postprocessSchema = z.string().transform((str) => {
  return {
    original: str,
    length: str.length,
    uppercase: str.toUpperCase()
  };
});

const preprocessResult = preprocessSchema.parse('  HELLO  '); // ✅ 'hello'
const postprocessResult = postprocessSchema.parse('hello'); // ✅ { original: 'hello', length: 5, uppercase: 'HELLO' }

// 14. Pipeline Transformations
const dateToTimestamp = z.date().transform((date) => date.getTime());
const timestampToDate = z.number().transform((timestamp) => new Date(timestamp));

const timestamp = dateToTimestamp.parse(new Date()); // ✅ timestamp (number)
const date = timestampToDate.parse(Date.now()); // ✅ Date object

// 15. Error Customization
const customErrorSchema = z.string({
  errorMap: (issue, ctx) => {
    if (issue.code === z.ZodIssueCode.invalid_string) {
      return { message: 'Custom error message for invalid string' };
    }
    if (issue.code === z.ZodIssueCode.too_small) {
      return { message: 'String is too short!' };
    }
    return { message: ctx.defaultError };
  },
}).min(5);

try {
  customErrorSchema.parse('abc'); // ❌ Custom error
} catch (error) {
  console.log('Custom error message');
}

// 16. Advanced Primitive Examples

// Password validation with multiple requirements
const passwordSchema = z.string()
  .min(8, 'Password must be at least 8 characters')
  .max(100, 'Password must be less than 100 characters')
  .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
  .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
  .regex(/[0-9]/, 'Password must contain at least one number')
  .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character');

// Username validation
const usernameSchema = z.string()
  .min(3, 'Username must be at least 3 characters')
  .max(20, 'Username must be less than 20 characters')
  .regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores')
  .refine((username) => !username.startsWith('_'), {
    message: 'Username cannot start with an underscore'
  })
  .refine((username) => !username.endsWith('_'), {
    message: 'Username cannot end with an underscore'
  });

// Phone number validation
const phoneNumberSchema = z.string()
  .regex(/^+?[1-9]d{1,14}$/, 'Invalid phone number format')
  .transform((phone) => {
    // Normalize phone number format
    return phone.replace(/[^0-9+]/g, '');
  });

// URL validation with protocols
const urlSchema = z.string().url().refine((url) => {
  const protocols = ['http:', 'https:', 'ftp:'];
  try {
    const parsedUrl = new URL(url);
    return protocols.includes(parsedUrl.protocol);
  } catch {
    return false;
  }
}, {
  message: 'URL must use http, https, or ftp protocol'
});

// Color validation
const colorHexSchema = z.string()
  .regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, 'Invalid hex color format')
  .transform((hex) => hex.toLowerCase());

const colorRgbSchema = z.string()
  .regex(/^rgb(s*d+s*,s*d+s*,s*d+s*)$/i, 'Invalid RGB format')
  .transform((rgb) => {
    const matches = rgb.match(/d+/g);
    if (!matches || matches.length !== 3) return null;
    return {
      r: parseInt(matches[0]),
      g: parseInt(matches[1]),
      b: parseInt(matches[2])
    };
  });

// Test advanced validation
try {
  passwordSchema.parse('MySecureP@ssw0rd!'); // ✅
  usernameSchema.parse('john_doe123'); // ✅
  phoneNumberSchema.parse('+1-555-123-4567'); // ✅
  urlSchema.parse('https://example.com'); // ✅
  colorHexSchema.parse('#FF5733'); // ✅
  colorRgbSchema.parse('rgb(255, 87, 51)'); // ✅
} catch (error) {
  console.error('Advanced validation error:', error);
}

// 17. Usage Examples

// Form validation function
function validateUserInput(userData: unknown) {
  const userSchema = z.object({
    name: z.string().min(2).max(50),
    email: z.string().email(),
    age: z.number().min(18).max(120),
    website: z.string().url().optional(),
    bio: z.string().max(500).optional()
  });

  const result = userSchema.safeParse(userData);

  if (result.success) {
    return { success: true, data: result.data };
  } else {
    return {
      success: false,
      errors: result.error.issues.map(issue => ({
        field: issue.path.join('.'),
        message: issue.message
      }))
    };
  }
}

// API response validation
function validateApiResponse(response: unknown) {
  const responseSchema = z.object({
    success: z.boolean(),
    data: z.unknown().optional(),
    error: z.string().optional(),
    timestamp: z.string()
  });

  return responseSchema.parse(response);
}

// Environment variable validation
function validateEnvVars(envVars: Record<string, unknown>) {
  const envSchema = z.object({
    NODE_ENV: z.enum(['development', 'production', 'test']),
    PORT: z.string().transform(Number).pipe(z.number().positive()),
    DATABASE_URL: z.string().url(),
    JWT_SECRET: z.string().min(32),
    API_KEY: z.string().min(16),
    ENABLE_LOGGING: z.string().transform(Boolean).pipe(z.boolean()).default('true'),
    MAX_CONNECTIONS: z.string().transform(Number).pipe(z.number().positive()).default('10')
  });

  return envSchema.parse(envVars);
}

// Example usage
console.log('\n--- Usage Examples ---');

const userInput = {
  name: 'John Doe',
  email: '[email protected]',
  age: 25,
  website: 'https://johndoe.com',
  bio: 'Software developer'
};

const userValidation = validateUserInput(userInput);
console.log('User validation:', userValidation);

const apiResponse = {
  success: true,
  data: { message: 'Hello World' },
  timestamp: new Date().toISOString()
};

const validatedResponse = validateApiResponse(apiResponse);
console.log('API response validation:', validatedResponse);

const envVars = {
  NODE_ENV: 'development',
  PORT: '3000',
  DATABASE_URL: 'https://localhost:5432/mydb',
  JWT_SECRET: 'my-super-secret-jwt-key-32-chars-long',
  API_KEY: 'my-api-key-16-chars'
};

const validatedEnv = validateEnvVars(envVars);
console.log('Environment validation:', validatedEnv);

export {
  stringSchema,
  numberSchema,
  booleanSchema,
  dateSchema,
  userSchema: z.object({
    name: z.string().min(2).max(50),
    email: z.string().email(),
    age: z.number().min(18).max(120),
    website: z.string().url().optional(),
    bio: z.string().max(500).optional()
  }),
  passwordSchema,
  usernameSchema,
  validateUserInput,
  validateApiResponse,
  validateEnvVars
};

💻 Zod 对象和数组 typescript

🟡 intermediate ⭐⭐⭐

复杂的Zod模式,用于对象、数组、嵌套结构和高级验证模式

⏱️ 40 min 🏷️ zod, validation, typescript, schemas, objects
Prerequisites: Zod basics, TypeScript, Advanced data structures
// Zod Objects and Arrays Example

import { z } from 'zod';

// 1. Basic Object Schema
const userSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().min(13).max(120),
  isActive: z.boolean().default(true),
  createdAt: z.date().default(new Date()),
});

type User = z.infer<typeof userSchema>;

const validUser = {
  id: '550e8400-e29b-41d4-a716-446655440000',
  name: 'John Doe',
  email: '[email protected]',
  age: 30,
  isActive: true,
  createdAt: new Date()
};

console.log(userSchema.parse(validUser)); // ✅

// 2. Partial Object (for updates)
const updateUserSchema = userSchema.partial();

const updateData = {
  name: 'Jane Doe',
  age: 28
};

console.log(updateUserSchema.parse(updateData)); // ✅

// 3. Required Object (no optional/extra properties)
const strictUserSchema = z.object({
  name: z.string(),
  email: z.string().email()
}).strict();

console.log(strictUserSchema.parse({ name: 'John', email: '[email protected]' })); // ✅
// strictUserSchema.parse({ name: 'John', email: '[email protected]', extra: 'value' }); // ❌

// 4. Object with Passthrough (allow extra properties)
const passthroughUserSchema = z.object({
  name: z.string(),
  email: z.string().email()
}).passthrough();

console.log(passthroughUserSchema.parse({
  name: 'John',
  email: '[email protected]',
  role: 'admin',
  department: 'IT'
})); // ✅, preserves extra properties

// 5. Object with Catchall (custom handling for extra properties)
const catchallUserSchema = z.object({
  name: z.string(),
  email: z.string().email()
}).catchall(z.string());

console.log(catchallUserSchema.parse({
  name: 'John',
  email: '[email protected]',
  role: 'admin', // ✅, string value
  score: 100 // ❌, not a string
}));

// 6. Array Schemas
const stringArraySchema = z.array(z.string());
const numberArraySchema = z.array(z.number());
const userArraySchema = z.array(userSchema);

console.log(stringArraySchema.parse(['hello', 'world'])); // ✅
console.log(numberArraySchema.parse([1, 2, 3])); // ✅
console.log(userArraySchema.parse([validUser, validUser])); // ✅

// 7. Array Constraints
const minLengthArray = z.array(z.string()).min(2);
const maxLengthArray = z.array(z.number()).max(5);
const exactLengthArray = z.array(z.string()).length(3);

console.log(minLengthArray.parse(['a', 'b', 'c'])); // ✅
console.log(maxLengthArray.parse([1, 2, 3])); // ✅
console.log(exactLengthArray.parse(['x', 'y', 'z'])); // ✅

// 8. Non-empty Array
const nonEmptyArray = z.array(z.string()).nonempty();
console.log(nonEmptyArray.parse(['hello'])); // ✅
// nonEmptyArray.parse([]); // ❌

// 9. Set-like Array (unique values)
const uniqueArray = z.array(z.string()).refine(
  (arr) => new Set(arr).size === arr.length,
  { message: 'Array must contain unique values' }
);

console.log(uniqueArray.parse(['a', 'b', 'c'])); // ✅
// uniqueArray.parse(['a', 'b', 'a']); // ❌

// 10. Nested Objects
const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
  state: z.string().length(2),
  zipCode: z.string().regex(/^d{5}(-d{4})?$/),
  country: z.string().default('USA'),
  coordinates: z.object({
    lat: z.number().min(-90).max(90),
    lng: z.number().min(-180).max(180)
  }).optional()
});

const userWithAddressSchema = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  address: addressSchema,
  phoneNumbers: z.array(z.string().regex(/^+?[1-9]d{1,14}$/)).optional(),
  metadata: z.record(z.unknown()).optional()
});

const userWithAddress = {
  id: '550e8400-e29b-41d4-a716-446655440000',
  name: 'John Doe',
  email: '[email protected]',
  address: {
    street: '123 Main St',
    city: 'New York',
    state: 'NY',
    zipCode: '10001',
    coordinates: { lat: 40.7128, lng: -74.0060 }
  },
  phoneNumbers: ['+1-555-123-4567'],
  metadata: {
    lastLogin: new Date().toISOString(),
    preferences: { theme: 'dark' }
  }
};

console.log(userWithAddressSchema.parse(userWithAddress)); // ✅

// 11. Array of Objects
const postSchema = z.object({
  id: z.string().uuid(),
  title: z.string().min(1).max(200),
  content: z.string().max(10000),
  authorId: z.string().uuid(),
  tags: z.array(z.string().min(1).max(20)).max(5),
  published: z.boolean().default(false),
  createdAt: z.date(),
  updatedAt: z.date().optional()
});

const postsSchema = z.array(postSchema);

const posts = [
  {
    id: '550e8400-e29b-41d4-a716-446655440001',
    title: 'Hello World',
    content: 'This is my first post',
    authorId: '550e8400-e29b-41d4-a716-446655440000',
    tags: ['intro', 'hello'],
    published: true,
    createdAt: new Date()
  },
  {
    id: '550e8400-e29b-41d4-a716-446655440002',
    title: 'Second Post',
    content: 'Another interesting post',
    authorId: '550e8400-e29b-41d4-a716-446655440000',
    tags: ['updates', 'announcement'],
    published: false,
    createdAt: new Date()
  }
];

console.log(postsSchema.parse(posts)); // ✅

// 12. Discriminated Unions
const baseEventSchema = z.object({
  id: z.string().uuid(),
  timestamp: z.date(),
  userId: z.string().uuid()
});

const userLoginEventSchema = baseEventSchema.extend({
  type: z.literal('user.login'),
  ipAddress: z.string().ip(),
  userAgent: z.string()
});

const userLogoutEventSchema = baseEventSchema.extend({
  type: z.literal('user.logout'),
  sessionDuration: z.number().positive()
});

const postCreatedEventSchema = baseEventSchema.extend({
  type: z.literal('post.created'),
  postId: z.string().uuid(),
  postTitle: z.string()
});

const eventSchema = z.discriminatedUnion('type', [
  userLoginEventSchema,
  userLogoutEventSchema,
  postCreatedEventSchema
]);

type Event = z.infer<typeof eventSchema>;

// Valid events
const loginEvent = {
  id: '550e8400-e29b-41d4-a716-446655440003',
  timestamp: new Date(),
  userId: '550e8400-e29b-41d4-a716-446655440000',
  type: 'user.login' as const,
  ipAddress: '192.168.1.1',
  userAgent: 'Mozilla/5.0...'
};

const logoutEvent = {
  id: '550e8400-e29b-41d4-a716-446655440004',
  timestamp: new Date(),
  userId: '550e8400-e29b-41d4-a716-446655440000',
  type: 'user.logout' as const,
  sessionDuration: 3600
};

const postEvent = {
  id: '550e8400-e29b-41d4-a716-446655440005',
  timestamp: new Date(),
  userId: '550e8400-e29b-41d4-a716-446655440000',
  type: 'post.created' as const,
  postId: '550e8400-e29b-41d4-a716-446655440001',
  postTitle: 'Hello World'
};

console.log(eventSchema.parse(loginEvent)); // ✅
console.log(eventSchema.parse(logoutEvent)); // ✅
console.log(eventSchema.parse(postEvent)); // ✅

// 13. Record Schema (key-value object)
const configSchema = z.record(z.string(), z.unknown());

const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3,
  features: {
    darkMode: true,
    notifications: false
  }
};

console.log(configSchema.parse(config)); // ✅

// More specific record schema
const themeConfigSchema = z.record(z.enum(['primary', 'secondary', 'success', 'danger', 'warning', 'info']), z.string());

const themeConfig = {
  header: 'primary',
  sidebar: 'secondary',
  buttons: 'success',
  alerts: 'danger',
  notifications: 'warning',
  links: 'info'
};

console.log(themeConfigSchema.parse(themeConfig)); // ✅

// 14. Tuple Schema
const coordinateSchema = z.tuple([z.number(), z.number()]);
const rgbColorSchema = z.tuple([z.number().min(0).max(255), z.number().min(0).max(255), z.number().min(0).max(255)]);

console.log(coordinateSchema.parse([10, 20])); // ✅
console.log(rgbColorSchema.parse([255, 128, 0])); // ✅

// 15. Optional Array Elements
const profileSchema = z.object({
  username: z.string(),
  email: z.string().email(),
  socialLinks: z.array(z.object({
    platform: z.string(),
    url: z.string().url()
  })).optional(),
  skills: z.array(z.string()).optional()
});

const profile = {
  username: 'johndoe',
  email: '[email protected]',
  socialLinks: [
    { platform: 'twitter', url: 'https://twitter.com/johndoe' },
    { platform: 'github', url: 'https://github.com/johndoe' }
  ],
  skills: ['TypeScript', 'React', 'Node.js']
};

console.log(profileSchema.parse(profile)); // ✅

// 16. Recursive Schema (self-referencing)
const categorySchema = z.object({
  id: z.string(),
  name: z.string(),
  description: z.string().optional(),
  parentCategory: z.string().optional()
});

const categoryWithChildren = categorySchema.extend({
  subcategories: z.lazy(() => z.array(categoryWithChildren)).optional()
});

// Valid category with nested children
const categoryTree = {
  id: 'cat1',
  name: 'Technology',
  description: 'All things tech',
  subcategories: [
    {
      id: 'cat2',
      name: 'Programming',
      subcategories: [
        {
          id: 'cat3',
          name: 'JavaScript',
          parentCategory: 'cat2'
        },
        {
          id: 'cat4',
          name: 'Python',
          parentCategory: 'cat2'
        }
      ]
    },
    {
      id: 'cat5',
      name: 'Hardware',
      parentCategory: 'cat1'
    }
  ]
};

console.log(categoryWithChildren.parse(categoryTree)); // ✅

// 17. Complex Validation Examples

// Product schema with inventory management
const productSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1).max(100),
  description: z.string().max(1000),
  price: z.number().positive(),
  categories: z.array(z.string().uuid()).min(1),
  variants: z.array(z.object({
    id: z.string().uuid(),
    name: z.string(),
    sku: z.string(),
    price: z.number().positive().optional(),
    inventory: z.object({
      quantity: z.number().min(0),
      lowStockThreshold: z.number().min(0),
      trackInventory: z.boolean()
    }),
    attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
    images: z.array(z.object({
      url: z.string().url(),
      alt: z.string().optional(),
      isPrimary: z.boolean().default(false)
    }))
  })).min(1),
  tags: z.array(z.string()).max(10).optional(),
  metadata: z.object({
    weight: z.number().positive().optional(),
    dimensions: z.object({
      length: z.number().positive(),
      width: z.number().positive(),
      height: z.number().positive(),
      unit: z.enum(['cm', 'in', 'mm']).default('cm')
    }).optional(),
    seo: z.object({
      title: z.string().max(60).optional(),
      description: z.string().max(160).optional(),
      keywords: z.array(z.string()).max(10).optional()
    }).optional()
  }).optional()
});

// Order schema with line items
const orderItemSchema = z.object({
  productId: z.string().uuid(),
  variantId: z.string().uuid(),
  quantity: z.number().positive().max(100),
  price: z.number().positive(),
  discounts: z.array(z.object({
    type: z.enum(['percentage', 'fixed']),
    value: z.number().positive(),
    reason: z.string().optional()
  })).optional()
});

const orderSchema = z.object({
  id: z.string().uuid(),
  customerId: z.string().uuid(),
  items: z.array(orderItemSchema).min(1),
  status: z.enum(['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled']),
  shippingAddress: addressSchema,
  billingAddress: addressSchema,
  paymentMethod: z.object({
    type: z.enum(['credit_card', 'debit_card', 'paypal', 'stripe']),
    lastFour: z.string().length(4).regex(/^d+$/),
    brand: z.string().optional()
  }),
  subtotal: z.number().positive(),
  tax: z.number().nonnegative(),
  shipping: z.number().nonnegative(),
  total: z.number().positive(),
  createdAt: z.date(),
  shippedAt: z.date().optional(),
  deliveredAt: z.date().optional()
});

// Validation functions for complex scenarios
function validateProductInventory(product: unknown, requestedQuantity: number) {
  const parsedProduct = productSchema.parse(product);

  for (const variant of parsedProduct.variants) {
    if (variant.inventory.quantity < requestedQuantity) {
      throw new Error(`Insufficient inventory for variant ${variant.name}. Available: ${variant.inventory.quantity}, Requested: ${requestedQuantity}`);
    }

    if (variant.inventory.trackInventory && variant.inventory.quantity <= variant.inventory.lowStockThreshold) {
      console.warn(`Low stock warning for variant ${variant.name}. Current stock: ${variant.inventory.quantity}`);
    }
  }

  return parsedProduct;
}

function validateOrderTotal(order: unknown) {
  const parsedOrder = orderSchema.parse(order);

  const calculatedSubtotal = parsedOrder.items.reduce((sum, item) => {
    const itemTotal = item.price * item.quantity;
    const discountTotal = (item.discounts || []).reduce((discountSum, discount) => {
      if (discount.type === 'percentage') {
        return discountSum + (itemTotal * discount.value / 100);
      } else {
        return discountSum + discount.value;
      }
    }, 0);
    return sum + (itemTotal - discountTotal);
  }, 0);

  if (Math.abs(calculatedSubtotal - parsedOrder.subtotal) > 0.01) {
    throw new Error(`Subtotal mismatch. Calculated: ${calculatedSubtotal}, Provided: ${parsedOrder.subtotal}`);
  }

  const calculatedTotal = calculatedSubtotal + parsedOrder.tax + parsedOrder.shipping;
  if (Math.abs(calculatedTotal - parsedOrder.total) > 0.01) {
    throw new Error(`Total mismatch. Calculated: ${calculatedTotal}, Provided: ${parsedOrder.total}`);
  }

  return parsedOrder;
}

// Test complex validation
try {
  const testProduct = {
    id: '550e8400-e29b-41d4-a716-446655440006',
    name: 'Wireless Headphones',
    description: 'High-quality wireless headphones with noise cancellation',
    price: 99.99,
    categories: ['cat1', 'cat2'],
    variants: [
      {
        id: 'var1',
        name: 'Black',
        sku: 'WH-BLK-001',
        inventory: {
          quantity: 50,
          lowStockThreshold: 10,
          trackInventory: true
        },
        attributes: { color: 'black', wireless: true },
        images: [
          { url: 'https://example.com/headphones-black.jpg', alt: 'Black wireless headphones', isPrimary: true }
        ]
      }
    ]
  };

  validateProductInventory(testProduct, 5); // ✅

  const testOrder = {
    id: 'order1',
    customerId: 'customer1',
    items: [
      {
        productId: 'product1',
        variantId: 'var1',
        quantity: 2,
        price: 99.99
      }
    ],
    status: 'pending' as const,
    shippingAddress: {
      street: '123 Main St',
      city: 'New York',
      state: 'NY',
      zipCode: '10001'
    },
    billingAddress: {
      street: '123 Main St',
      city: 'New York',
      state: 'NY',
      zipCode: '10001'
    },
    paymentMethod: {
      type: 'credit_card' as const,
      lastFour: '1234',
      brand: 'Visa'
    },
    subtotal: 199.98,
    tax: 16.00,
    shipping: 5.00,
    total: 220.98,
    createdAt: new Date()
  };

  validateOrderTotal(testOrder); // ✅
} catch (error) {
  console.error('Complex validation error:', error);
}

export {
  userSchema,
  addressSchema,
  userWithAddressSchema,
  postSchema,
  postsSchema,
  eventSchema,
  productSchema,
  orderItemSchema,
  orderSchema,
  validateProductInventory,
  validateOrderTotal,
  type User,
  type Event
};

💻 Zod 高级模式 typescript

🔴 complex ⭐⭐⭐⭐⭐

高级Zod模式,包括自定义验证器、错误处理、模式组合、中间件和实际用例

⏱️ 60 min 🏷️ zod, validation, patterns, typescript, advanced
Prerequisites: Advanced Zod, TypeScript, Design patterns, Async/await
// Zod Advanced Patterns and Real-World Examples

import { z, ZodIssue, ZodSchema } from 'zod';

// 1. Custom Validation with Detailed Error Messages

// Enhanced string validation with multiple custom rules
const enhancedStringSchema = z.string()
  .min(3, 'String must be at least 3 characters long')
  .max(100, 'String must be at most 100 characters long')
  .regex(/^[a-zA-Z0-9\s-_]+$/, 'String can only contain letters, numbers, spaces, hyphens, and underscores')
  .refine((val) => val.trim() === val, {
    message: 'String cannot have leading or trailing whitespace'
  })
  .refine((val) => {
    const wordCount = val.trim().split(/\s+/).length;
    return wordCount >= 2;
  }, {
    message: 'String must contain at least 2 words'
  })
  .transform((val) => val.trim());

// Custom ID validator with format checking
const idSchema = z.string()
  .regex(/^[a-zA-Z0-9_-]+$/, 'ID can only contain letters, numbers, hyphens, and underscores')
  .min(3, 'ID must be at least 3 characters long')
  .max(50, 'ID must be at most 50 characters long')
  .refine((val) => /^[a-zA-Z]/.test(val), {
    message: 'ID must start with a letter'
  })
  .refine((val) => !/[-_]{2,}/.test(val), {
    message: 'ID cannot contain consecutive hyphens or underscores'
  });

// 2. Schema Composition and Reuse

// Base field schemas for reuse
const baseFieldSchemas = {
  id: idSchema,
  name: z.string().min(1).max(100),
  description: z.string().max(500).optional(),
  createdAt: z.date(),
  updatedAt: z.date().optional(),
  isActive: z.boolean().default(true)
};

// Timestamp schema with validation
const timestampSchema = z.date()
  .refine((date) => date <= new Date(), {
    message: 'Date cannot be in the future'
  })
  .refine((date) => date >= new Date('2000-01-01'), {
    message: 'Date must be after year 2000'
  });

// 3. Advanced Error Handling

// Custom error formatting
const formatZodError = (error: z.ZodError) => {
  return {
    success: false,
    error: {
      code: error.issues[0]?.code || 'VALIDATION_ERROR',
      message: error.issues[0]?.message || 'Validation failed',
      field: error.issues[0]?.path?.join('.') || 'unknown',
      details: error.issues.map(issue => ({
        field: issue.path?.join('.') || 'unknown',
        code: issue.code,
        message: issue.message,
        received: issue.received,
        expected: issue.expected
      }))
    }
  };
};

// Custom error class for validation
class ValidationError extends Error {
  constructor(
    message: string,
    public readonly code: string,
    public readonly field?: string,
    public readonly details?: ZodIssue[]
  ) {
    super(message);
    this.name = 'ValidationError';
  }
}

// Enhanced safe parse with custom error handling
function safeParseWithCustomError<T>(schema: ZodSchema<T>, data: unknown): T {
  const result = schema.safeParse(data);

  if (!result.success) {
    const firstError = result.error.issues[0];
    throw new ValidationError(
      firstError.message,
      firstError.code,
      firstError.path?.join('.'),
      result.error.issues
    );
  }

  return result.data;
}

// 4. Middleware Pattern for Validation

// Validation middleware type
type ValidationMiddleware<T> = (data: unknown) => T;

// Create validation middleware
function createValidationMiddleware<T>(schema: ZodSchema<T>): ValidationMiddleware<T> {
  return (data: unknown): T => {
    return safeParseWithCustomError(schema, data);
  };
}

// Higher-order function for validation
function withValidation<T>(
  schema: ZodSchema<T>,
  fn: (validatedData: T) => unknown
) {
  const validate = createValidationMiddleware(schema);

  return (data: unknown) => {
    const validatedData = validate(data);
    return fn(validatedData);
  };
}

// 5. Async Validation Patterns

// Async validator for checking uniqueness
async function createUniqueEmailValidator(existingEmails: Set<string>) {
  return async (email: string) => {
    // Simulate database check
    await new Promise(resolve => setTimeout(resolve, 100));

    if (existingEmails.has(email.toLowerCase())) {
      throw new Error('Email already exists');
    }

    return true;
  };
}

// Async schema with custom validation
async function createUserSchema(existingEmails: Set<string> = new Set()) {
  const uniqueEmailValidator = await createUniqueEmailValidator(existingEmails);

  return z.object({
    email: z.string().email().refine(uniqueEmailValidator, {
      message: 'Email must be unique'
    }),
    username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
    password: z.string()
      .min(8)
      .regex(/[A-Z]/, 'Password must contain uppercase')
      .regex(/[a-z]/, 'Password must contain lowercase')
      .regex(/[0-9]/, 'Password must contain number')
      .regex(/[^A-Za-z0-9]/, 'Password must contain special character')
  });
}

// 6. Conditional Validation

// Conditional object schema based on user type
function createUserProfileSchema(isAdmin: boolean) {
  const baseProfile = z.object({
    firstName: z.string().min(1),
    lastName: z.string().min(1),
    email: z.string().email(),
    bio: z.string().max(500).optional()
  });

  if (isAdmin) {
    return baseProfile.extend({
      adminLevel: z.enum(['junior', 'senior', 'lead']),
      permissions: z.array(z.string()).min(1),
      department: z.string().min(1)
    });
  }

  return baseProfile.extend({
    preferences: z.object({
      theme: z.enum(['light', 'dark']),
      notifications: z.boolean(),
      language: z.string().length(2)
    })
  });
}

// 7. Schema Refactoring with Composition

// Address schema
const addressSchema = z.object({
  street: z.string().min(1),
  city: z.string().min(1),
  state: z.string().length(2),
  zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
  country: z.string().default('USA'),
  addressType: z.enum(['shipping', 'billing', 'both'])
});

// Contact schema
const contactSchema = z.object({
  phone: z.string().regex(/^\+?[1-9]\d{1,14}$/).optional(),
  email: z.string().email(),
  website: z.string().url().optional(),
  socialMedia: z.record(z.string().url()).optional()
});

// User schema composed from smaller schemas
const createUserSchema = () => z.object({
  ...baseFieldSchemas,
  contact: contactSchema,
  addresses: z.array(addressSchema).min(1),
  role: z.enum(['user', 'admin', 'moderator']).default('user'),
  profile: z.object({
    avatar: z.string().url().optional(),
    timezone: z.string(),
    locale: z.string().default('en-US')
  })
});

// 8. Real-World API Request/Response Validation

// API Request schemas
const PaginationSchema = z.object({
  page: z.coerce.number().positive().default(1),
  limit: z.coerce.number().positive().max(100).default(20),
  sortBy: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
  sortOrder: z.enum(['asc', 'desc']).default('desc')
});

const FilterSchema = z.object({
  status: z.enum(['active', 'inactive', 'all']).optional(),
  category: z.string().optional(),
  dateFrom: z.string().datetime().optional(),
  dateTo: z.string().datetime().optional(),
  search: z.string().optional()
});

// API Request wrapper
const ApiRequestSchema = z.object({
  pagination: PaginationSchema,
  filters: FilterSchema,
  include: z.array(z.string()).optional()
});

// API Response schemas
const ApiSuccessSchema = <T>(dataSchema: ZodSchema<T>) => z.object({
  success: z.literal(true),
  data: dataSchema,
  pagination: z.object({
    currentPage: z.number(),
    totalPages: z.number(),
    totalItems: z.number(),
    hasNext: z.boolean(),
    hasPrev: z.boolean()
  }).optional(),
  meta: z.record(z.unknown()).optional()
});

const ApiErrorSchema = z.object({
  success: z.literal(false),
  error: z.object({
    code: z.string(),
    message: z.string(),
    details: z.array(z.object({
      field: z.string(),
      code: z.string(),
      message: z.string()
    })).optional()
  }),
  meta: z.record(z.unknown()).optional()
});

// Generic API response schema
const ApiResponseSchema = <T>(dataSchema: ZodSchema<T>) =>
  z.union([ApiSuccessSchema(dataSchema), ApiErrorSchema]);

// 9. Database Model Validation

// UUID validator with fallback
const uuidSchema = z.string().uuid().or(z.string().length(36)).transform((val) => {
  // Handle both UUID and string representation
  return val.length === 36 ? val : val;
});

// Entity status validator
const EntityStatusSchema = z.enum(['active', 'inactive', 'archived', 'pending']).default('active');

// Soft delete mixin
const withSoftDelete = <T extends z.ZodRawShape>(
  schema: T
) => z.object(schema).extend({
  deletedAt: z.date().nullable().default(null),
  deletedBy: uuidSchema.nullable().default(null)
});

// Audit fields mixin
const withAuditFields = <T extends z.ZodRawShape>(
  schema: T
) => z.object(schema).extend({
  createdAt: z.date().default(() => new Date()),
  updatedAt: z.date().default(() => new Date()),
  createdBy: uuidSchema,
  updatedBy: uuidSchema.optional()
});

// 10. Environment Configuration Validation

// Environment type discriminator
const EnvironmentSchema = z.enum(['development', 'staging', 'production', 'test']);

// Database configuration
const DatabaseConfigSchema = z.object({
  url: z.string().url(),
  ssl: z.boolean().default(true),
  maxConnections: z.number().positive().default(10),
  connectionTimeout: z.number().positive().default(30000),
  idleTimeout: z.number().positive().default(300000)
});

// Server configuration
const ServerConfigSchema = z.object({
  port: z.number().positive().default(3000),
  host: z.string().default('localhost'),
  cors: z.object({
    origin: z.union([z.string(), z.array(z.string())]).default('*'),
    credentials: z.boolean().default(false),
    methods: z.array(z.enum(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])).default(['GET', 'POST'])
  }).default({}),
  rateLimit: z.object({
    windowMs: z.number().positive().default(900000), // 15 minutes
    maxRequests: z.number().positive().default(100)
  }).optional()
});

// Complete environment configuration
const EnvironmentConfigSchema = z.object({
  nodeEnv: EnvironmentSchema,
  server: ServerConfigSchema,
  database: DatabaseConfigSchema,
  auth: z.object({
    jwtSecret: z.string().min(32),
    jwtExpiresIn: z.string().default('24h'),
    bcryptRounds: z.number().positive().min(10).max(12).default(12)
  }),
  redis: z.object({
    url: z.string().url().optional(),
    host: z.string().default('localhost'),
    port: z.number().positive().default(6379),
    password: z.string().optional(),
    db: z.number().default(0)
  }).optional(),
  features: z.object({
    enableMetrics: z.boolean().default(false),
    enableLogging: z.boolean().default(true),
    enableCache: z.boolean().default(true),
    enableRateLimit: z.boolean().default(true)
  }).default({})
});

// 11. Form Validation Patterns

// Multi-step form schema
const step1Schema = z.object({
  firstName: z.string().min(1),
  lastName: z.string().min(1),
  email: z.string().email(),
  phone: z.string().regex(/^\+?[1-9]\d{1,14}$/)
});

const step2Schema = z.object({
  address: addressSchema,
  preferences: z.object({
    newsletter: z.boolean().default(false),
    marketing: z.boolean().default(false)
  })
});

const step3Schema = z.object({
  paymentMethod: z.enum(['credit_card', 'paypal', 'bank_transfer']),
  terms: z.boolean().refine(val => val === true, {
    message: 'You must accept the terms and conditions'
  })
});

// Multi-step form validator
function validateMultiStepForm(step: number, data: unknown) {
  switch (step) {
    case 1:
      return step1Schema.parse(data);
    case 2:
      return step2Schema.parse(data);
    case 3:
      return step3Schema.parse(data);
    default:
      throw new Error('Invalid step number');
  }
}

// 12. Real-World Usage Examples

// User registration service
class UserService {
  private existingEmails = new Set<string>();
  private existingUsernames = new Set<string>();

  async registerUser(userData: unknown) {
    const userSchema = await this.createUserRegistrationSchema();
    const validatedUser = safeParseWithCustomError(userSchema, userData);

    // Additional business logic
    await this.checkDuplicateEmail(validatedUser.email);
    await this.checkDuplicateUsername(validatedUser.username);

    const user = await this.createUserInDatabase(validatedUser);

    // Update tracking sets
    this.existingEmails.add(user.email.toLowerCase());
    this.existingUsernames.add(user.username.toLowerCase());

    return user;
  }

  private async createUserRegistrationSchema() {
    const uniqueEmailValidator = await this.createUniqueEmailValidator();

    return z.object({
      username: z.string()
        .min(3)
        .max(20)
        .regex(/^[a-zA-Z0-9_]+$/)
        .refine(async (username) => {
          await new Promise(resolve => setTimeout(resolve, 50)); // Simulate async check
          return !this.existingUsernames.has(username.toLowerCase());
        }, {
          message: 'Username already exists'
        }),
      email: z.string().email(),
      password: z.string().min(8),
      confirmPassword: z.string()
    }).refine((data) => data.password === data.confirmPassword, {
      message: 'Passwords do not match',
      path: ['confirmPassword']
    });
  }

  private async createUniqueEmailValidator() {
    return async (email: string) => {
      await new Promise(resolve => setTimeout(resolve, 50));
      return !this.existingEmails.has(email.toLowerCase());
    };
  }

  private async checkDuplicateEmail(email: string) {
    if (this.existingEmails.has(email.toLowerCase())) {
      throw new ValidationError('Email already exists', 'DUPLICATE_EMAIL', 'email');
    }
  }

  private async checkDuplicateUsername(username: string) {
    if (this.existingUsernames.has(username.toLowerCase())) {
      throw new ValidationError('Username already exists', 'DUPLICATE_USERNAME', 'username');
    }
  }

  private async createUserInDatabase(userData: any) {
    // Simulate database insertion
    await new Promise(resolve => setTimeout(resolve, 100));

    return {
      id: Math.random().toString(36).substr(2, 9),
      ...userData,
      createdAt: new Date(),
      updatedAt: new Date()
    };
  }
}

// API request/response handler
class ApiController {
  handleRequest(request: unknown, responseSchema: ZodSchema<any>) {
    try {
      const validatedRequest = safeParseWithCustomError(
        ApiRequestSchema,
        request
      );

      // Process request...
      const data = await this.processRequest(validatedRequest);

      const validatedResponse = ApiSuccessSchema(responseSchema).parse({
        success: true,
        data
      });

      return validatedResponse;

    } catch (error) {
      if (error instanceof ValidationError) {
        return ApiErrorSchema.parse({
          success: false,
          error: {
            code: error.code,
            message: error.message,
            details: error.details?.map(issue => ({
              field: issue.field,
              code: issue.code,
              message: issue.message
            }))
          }
        });
      }

      // Handle other unexpected errors
      return ApiErrorSchema.parse({
        success: false,
        error: {
          code: 'INTERNAL_ERROR',
          message: 'An unexpected error occurred'
        }
      });
    }
  }

  private async processRequest(request: any) {
    // Simulate async processing
    await new Promise(resolve => setTimeout(resolve, 50));
    return { message: 'Request processed successfully' };
  }
}

// 13. Testing with Zod

// Test utilities
function expectValidationSuccess<T>(schema: ZodSchema<T>, data: unknown): T {
  const result = schema.safeParse(data);
  if (!result.success) {
    throw new Error(`Expected validation to succeed, but it failed: ${result.error.message}`);
  }
  return result.data;
}

function expectValidationFailure<T>(schema: ZodSchema<T>, data: unknown, expectedError?: string): void {
  const result = schema.safeParse(data);
  if (result.success) {
    throw new Error('Expected validation to fail, but it succeeded');
  }
  if (expectedError && !result.error.message.includes(expectedError)) {
    throw new Error(`Expected error message to contain "${expectedError}", but got: ${result.error.message}`);
  }
}

// Test examples
function runTests() {
  console.log('Running Zod validation tests...');

  // Test success cases
  const validUser = expectValidationSuccess(userSchema, {
    id: '550e8400-e29b-41d4-a716-446655440000',
    name: 'John Doe',
    email: '[email protected]',
    age: 30
  });
  console.log('✅ Valid user test passed');

  // Test failure cases
  expectValidationFailure(
    userSchema,
    { id: 'invalid-uuid', name: '', email: 'invalid-email', age: -5 },
    'Expected string'
  );
  console.log('✅ Invalid user test passed');

  console.log('All tests completed successfully!');
}

// Execute tests
runTests();

// 14. Schema Documentation Generator

function generateSchemaDocumentation(schema: ZodSchema<any>, name: string): string {
  const description = schema._def.description || name;
  let docs = `# ${name}\n\n${description}\n\n## Schema Definition\n\n`;

  if (schema._def.typeName === 'ZodObject') {
    docs += '| Property | Type | Required | Description |\n';
    docs += '|---------|------|----------|-------------|\n';

    for (const [key, def] of Object.entries(schema._def.shape())) {
      const type = def._def.typeName;
      const required = !def._def.defaultValue;
      const description = def._def.description || '';

      docs += `| ${key} | ${type} | ${required ? 'Yes' : 'No'} | ${description} |\n`;
    }
  }

  return docs;
}

export {
  enhancedStringSchema,
  idSchema,
  formatZodError,
  ValidationError,
  safeParseWithCustomError,
  createValidationMiddleware,
  withValidation,
  ApiRequestSchema,
  ApiResponseSchema,
  EnvironmentConfigSchema,
  validateMultiStepForm,
  UserService,
  ApiController,
  expectValidationSuccess,
  expectValidationFailure,
  generateSchemaDocumentation
};