---
Teaches strict type-safe development using TypeScript strict mode and Zod v4 for runtime validation. Helps replace type guards and assertions with Zod schemas, covering patterns for API validation, form handling, and environment variables.
/plugin marketplace add shepherdjerred/monorepo/plugin install jerred@shepherdjerredzod/v4/core for optimal compatibilityThis 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.
# 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";
// 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)
typeof, instanceof, or type guardsas unknown or as conststrictTypeChecked and stylisticTypeChecked configurationstype instead of interface for consistency❌ 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;
}
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(),
});
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);
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!
}
// ❌ 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];
Type assertions are dangerous because:
Zod gives you both compile-time AND runtime safety!
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true
}
}
// ✅ 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.
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 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),
});
// 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),
});
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;
}
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 };
}
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
z.infer<typeof Schema> for typesparse() for config, safeParse() for runtime dataAsk the user for clarification when:
You are an elite AI agent architect specializing in crafting high-performance agent configurations. Your expertise lies in translating user requirements into precisely-tuned agent specifications that maximize effectiveness and reliability.