TypeScript provides excellent compile-time type safety, but it cannot protect you from invalid data at runtime. API responses, form inputs, and external data sources can contain unexpected values that bypass TypeScript's type checking. This leads to runtime errors, data corruption, and security vulnerabilities.
Zod is a TypeScript-first schema validation library that validates data at runtime and automatically infers TypeScript types from your schemas. This ensures your data matches your types not just during development, but also when your application is running in production.
// TypeScript can't protect you hereinterface User {id: number;email: string;age: number;}async function getUser(id: string): Promise<User> {const response = await fetch(`/api/users/${id}`);const data = await response.json();// ⚠️ What if the API returns { id: "abc", email: null, age: "25" }?// TypeScript won't catch this - it only checks compile time!return data; // Runtime error waiting to happen}// Later in your code...const user = await getUser("123");console.log(user.age.toFixed(2)); // 💥 Crashes if age is a string
import { z } from 'zod';// Define schema and infer typeconst UserSchema = z.object({id: z.number(),email: z.string().email(),age: z.number().min(0).max(150),});type User = z.infer<typeof UserSchema>;async function getUser(id: string): Promise<User> {const response = await fetch(`/api/users/${id}`);const data = await response.json();// Validate at runtime - throws if data is invalidconst user = UserSchema.parse(data);return user; // Guaranteed to be a valid User}// Your code is now safeconst user = await getUser("123");console.log(user.age.toFixed(2)); // ✅ Safe - age is guaranteed to be a number
import { z } from 'zod';import { useForm } from 'react-hook-form';import { zodResolver } from '@hookform/resolvers/zod';const FormSchema = z.object({email: z.string().email('Invalid email address'),password: z.string().min(8, 'Password must be at least 8 characters'),age: z.number().min(18, 'Must be 18 or older'),});type FormData = z.infer<typeof FormSchema>;function MyForm() {const { register, handleSubmit, formState: { errors } } = useForm<FormData>({resolver: zodResolver(FormSchema),});const onSubmit = (data: FormData) => {// data is guaranteed to be validconsole.log(data);};return (<form onSubmit={handleSubmit(onSubmit)}><input {...register('email')} />{errors.email && <span>{errors.email.message}</span>}<input type="password" {...register('password')} />{errors.password && <span>{errors.password.message}</span>}<input type="number" {...register('age', { valueAsNumber: true })} />{errors.age && <span>{errors.age.message}</span>}<button type="submit">Submit</button></form>);}
import { z } from 'zod';const EnvSchema = z.object({DATABASE_URL: z.string().url(),API_KEY: z.string().min(1),PORT: z.string().transform(Number).pipe(z.number().min(1000)),NODE_ENV: z.enum(['development', 'production', 'test']),});// Parse environment variables at startupconst env = EnvSchema.parse(process.env);// Now you have type-safe, validated environment variablesexport { env };
While other validation libraries exist (Yup, Joi, io-ts), Zod is the current recommendation because: