Type-Safe Development Agent
What's New in Zod v4 (2025)
- •14x Faster String Validation: Dramatic performance improvements for string parsing
- •7x Faster Arrays: Array validation significantly optimized
- •6.5x Faster Objects: Object parsing with major speed gains
- •57% Smaller Core: Bundle size reduced from ~12KB to ~5KB gzipped
- •@zod/mini Package: Tree-shakable minimal bundle (1.9KB gzipped)
- •TypeScript 5.5+ Required: Drops support for older TypeScript versions
- •Library Authors: Import from
zod/v4/corefor optimal compatibility
Overview
This agent teaches strict type-safe development patterns using TypeScript strict mode and Zod for runtime validation, based on coding standards from scout-for-lol and homelab repositories.
Performance Note: Zod v4 delivers production-ready performance with 14x faster strings, 7x faster arrays, and 6.5x faster objects. For ultra-minimal bundles, use @zod/mini (1.9KB) with tree-shaking.
Installation with Bun
# Standard Zod v4 (recommended)
bun add zod
# Minimal bundle for tree-shaking (1.9KB gzipped)
bun add @zod/mini
# For library authors targeting v4
import { z } from "zod/v4/core";
Using @zod/mini
// Tree-shakable imports (only includes what you use)
import { z } from "@zod/mini";
// Same API as full Zod
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
});
// Results in significantly smaller bundles
// Full Zod: ~5KB | @zod/mini: ~1.9KB (tree-shaken)
Core Principles
- •Use Zod for Runtime Validation: Prefer Zod schema validation over
typeof,instanceof, or type guards - •No Type Assertions: Avoid type assertions except
as unknownoras const - •Strict TypeScript: Use
strictTypeCheckedandstylisticTypeCheckedconfigurations - •Type Definitions: Use
typeinstead ofinterfacefor consistency
Zod Validation Patterns
Prefer Zod Over Type Guards
❌ Avoid: typeof operator
// Don't do this
function processValue(value: unknown) {
if (typeof value === "string") {
return value.toUpperCase();
}
}
✅ Prefer: Zod validation
import { z } from "zod";
function processValue(value: unknown) {
const result = z.string().safeParse(value);
if (result.success) {
return result.data.toUpperCase();
}
// Handle validation failure
return null;
}
Zod Schema Naming Convention
All Zod schemas must end with Schema suffix:
// ✅ Correct naming
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
});
type User = z.infer<typeof UserSchema>;
// ❌ Wrong - missing Schema suffix
const User = z.object({
id: z.string(),
email: z.string().email(),
});
safeParse vs parse
Use safeParse for most cases:
// ✅ Good - returns result object with .success
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data; // Type-safe access
console.log(user.email);
} else {
console.error("Validation failed:", result.error);
}
Use parse only when you want to throw:
// Use only for config validation or when failure should crash const config = ConfigSchema.parse(process.env);
Replace Common Type Checks
Array.isArray() → Zod
// ❌ Avoid
if (Array.isArray(value)) {
value.forEach(item => console.log(item));
}
// ✅ Prefer
const result = z.array(z.string()).safeParse(value);
if (result.success) {
result.data.forEach(item => console.log(item));
}
instanceof → Zod
// ❌ Avoid
if (err instanceof Error) {
console.log(err.message);
}
// ✅ Prefer
const result = z.instanceof(Error).safeParse(err);
if (result.success) {
console.log(result.data.message);
}
Number validation → Zod
// ❌ Avoid
if (Number.isInteger(value)) {
return value * 2;
}
// ✅ Prefer
const result = z.number().int().safeParse(value);
if (result.success) {
return result.data * 2;
}
Type predicates → Zod
// ❌ Avoid type guard functions
function isUser(value: unknown): value is User {
return typeof value === "object" && value !== null && "email" in value;
}
// ✅ Prefer Zod schema validation
const UserSchema = z.object({
email: z.string().email(),
name: z.string(),
});
const result = UserSchema.safeParse(value);
if (result.success) {
const user = result.data; // Type-safe!
}
Type Assertions Rules
Only Allow 'as unknown' and 'as const'
// ❌ Never do this - bypasses type safety
const user = data as User;
const id = value as string;
// ✅ Cast to unknown first, then validate
const data = response as unknown;
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data;
}
// ✅ Use 'as const' for literal types
const STATUSES = ["pending", "approved", "rejected"] as const;
type Status = (typeof STATUSES)[number];
Why No Type Assertions?
Type assertions are dangerous because:
- •They bypass TypeScript's type checking
- •They don't perform runtime validation
- •They can cause runtime crashes with wrong types
- •They hide bugs instead of catching them
Zod gives you both compile-time AND runtime safety!
TypeScript Configuration
tsconfig.json Best Practices
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}
Use 'type' Instead of 'interface'
// ✅ Prefer type
type User = {
id: string;
email: string;
};
type Admin = User & {
permissions: string[];
};
// ❌ Avoid interface
interface User {
id: string;
email: string;
}
Why? Types are more flexible (unions, intersections) and consistent with Zod's inferred types.
Advanced Zod Patterns
Discriminated Unions
const EventSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("user_created"),
userId: z.string(),
email: z.string().email(),
}),
z.object({
type: z.literal("user_deleted"),
userId: z.string(),
}),
]);
type Event = z.infer<typeof EventSchema>;
// Type-safe handling
function handleEvent(event: Event) {
switch (event.type) {
case "user_created":
console.log(event.email); // TypeScript knows email exists
break;
case "user_deleted":
console.log(event.userId); // No email here
break;
}
}
Transform and Refine
// Transform values during parsing
const DateSchema = z.string().transform((str) => new Date(str));
// Add custom validation
const PasswordSchema = z.string().min(8).refine(
(password) => /[A-Z]/.test(password),
{ message: "Password must contain uppercase letter" }
);
// Combine both
const UserInputSchema = z.object({
email: z.string().email().toLowerCase(), // transform to lowercase
createdAt: z.string().transform((str) => new Date(str)),
age: z.number().int().positive().max(150),
});
Reusable Schema Components
// Define reusable parts
const EmailSchema = z.string().email();
const UuidSchema = z.string().uuid();
const TimestampSchema = z.string().datetime();
// Compose into larger schemas
const UserSchema = z.object({
id: UuidSchema,
email: EmailSchema,
createdAt: TimestampSchema,
});
const TeamSchema = z.object({
id: UuidSchema,
name: z.string().min(1),
members: z.array(UserSchema),
});
Common Patterns
API Response Validation
const ApiResponseSchema = z.object({
data: UserSchema,
status: z.number().int(),
message: z.string().optional(),
});
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
const json = await response.json() as unknown;
const result = ApiResponseSchema.safeParse(json);
if (!result.success) {
throw new Error(`Invalid API response: ${result.error.message}`);
}
return result.data;
}
Form Validation
const FormDataSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
type FormData = z.infer<typeof FormDataSchema>;
function validateForm(data: unknown) {
const result = FormDataSchema.safeParse(data);
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors,
};
}
return { data: result.data };
}
Environment Variables
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().int().positive().default(3000),
NODE_ENV: z.enum(["development", "production", "test"]),
});
// Parse once at startup - throw if invalid
const env = EnvSchema.parse(process.env);
// Now env is fully typed!
console.log(env.PORT); // number
console.log(env.DATABASE_URL); // string
Best Practices
- •Define schemas early: Create Zod schemas alongside types
- •Single source of truth: Use
z.infer<typeof Schema>for types - •Validate at boundaries: API responses, user input, external data
- •Fail fast: Use
parse()for config,safeParse()for runtime data - •Compose schemas: Build complex schemas from simple reusable parts
- •Document schemas: Schemas serve as living documentation
- •Test schemas: Write tests for edge cases in your schemas
When to Ask for Help
Ask the user for clarification when:
- •The data structure is complex or ambiguous
- •Validation requirements are unclear
- •Performance is critical (Zod has overhead)
- •The type safety strategy conflicts with library requirements
- •Migration from existing type guards is extensive