VOOZH about

URL: https://blog.logrocket.com/when-use-zod-typescript-both-developers-guide/

โ‡ฑ TypeScript vs Zod: Clearing up validation confusion - LogRocket Blog


2025-10-06
1117
#typescript
Alexander Godwin
208089
116
๐Ÿ‘ Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

Imagine reviewing a pull request where a function validates user input using both TypeScript types and Zod schemas. You might wonder โ€” isnโ€™t that redundant? But if youโ€™ve ever been burned by a runtime error that slipped past TypeScript, you may also feel tempted to rely on Zod for everything.

๐Ÿ‘ Typescript or Zod for Validation?

The confusion often comes from mixing compile-time and runtime validation. Many developers see TypeScript and Zod as competing tools โ€” but in reality, they complement each other. Each provides a different kind of safety across your applicationโ€™s lifecycle.

TypeScript ensures type safety during development and the build process, while Zod validates untrusted data at runtime. Knowing when to use one or both helps create more reliable, consistent applications.

๐Ÿš€ Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

TypeScript vs. Zod: Different types of safety

TypeScript offers static analysis (compile-time)

TypeScript is your first line of defense, catching errors before they reach production. It provides:

  • Static analysis: Detects type mismatches and missing properties during development.
  • Developer experience: Enables autocomplete, refactoring, and inline documentation.
  • No runtime overhead: Type information is removed at compile time.

However, TypeScript canโ€™t validate runtime data. Once your application starts running, the types disappear โ€” leaving external inputs unchecked.

Zod for runtime validation

Zod fills that gap by validating the data your app receives from the outside world โ€” APIs, forms, configuration files, and more.

  • Runtime validation: Checks data at runtime, not just during development.
  • Type inference: Automatically generates TypeScript types from schemas.
  • Rich validation logic: Supports complex rules and custom error messages.

Zod follows the โ€œparse, donโ€™t validateโ€ philosophy โ€” it validates and safely transforms data into your expected shape in a single step.

Understanding the boundaries in your application

Choosing between TypeScript, Zod, or both depends on your dataโ€™s trust boundary:

  • Trusted data: Internal functions, controlled components โ€” TypeScript is enough.
  • Untrusted data: Anything from external sources (APIs, user input) โ€” use Zod.

Decision matrix: Choosing the right tool

Context TypeScript Only Zod Only Zod + TypeScript
Internal utilities Perfect fit Not needed Unnecessary complexity
Config files / JSON No runtime safety Good choice Best of both worlds
API boundaries Runtime blind spot Missing compile-time safety Essential
Complex forms No validation logic Handles validation well Maximum safety
3rd-party APIs Dangerous assumption Protects against changes Recommended
Database queries Shape can vary Validates results Type-safe queries

Example 1: API request and response validation

 
import { z } from 'zod';

const CreateUserSchema = z.object({
 email: z.string().email('Invalid email format'),
 name: z.string().min(2, 'Name must be at least 2 characters'),
 age: z.number().int().min(13, 'Must be at least 13 years old'),
 role: z.enum(['user', 'admin']).default('user')
});

type CreateUserRequest = z.infer;

const UserResponseSchema = z.object({
 id: z.string(),
 email: z.string(),
 name: z.string(),
 age: z.number(),
 role: z.enum(['user', 'admin']),
 createdAt: z.date()
});

type UserResponse = z.infer;

app.post('/users', async (req, res) => {
 try {
 const userData = CreateUserSchema.parse(req.body);
 const user = await createUser(userData);
 const validatedUser = UserResponseSchema.parse(user);
 
 res.json(validatedUser);
 } catch (error) {
 if (error instanceof z.ZodError) {
 return res.status(400).json({ errors: error.errors });
 }
 res.status(500).json({ error: 'Internal server error' });
 }
});

async function createUser(userData: CreateUserRequest): Promise {
 const user = {
 id: generateId(),
 ...userData,
 createdAt: new Date()
 };
 
 await db.users.create(user);
 return user;
}

Why this works:

  • Zod validates request and response data at runtime.
  • TypeScript infers types automatically โ€” no duplication.
  • Internal functions stay type-safe without extra runtime checks.
  • Every API response is validated before reaching the client.

Example 2: Complex client form

TypeScript-only approach (not recommended)

 
interface OnboardingForm {
 personalInfo: {
 firstName: string;
 lastName: string;
 email: string;
 phone?: string;
 };
 preferences: {
 newsletter: boolean;
 notifications: string[];
 theme: 'light' | 'dark';
 };
 account: {
 username: string;
 password: string;
 confirmPassword: string;
 };
}

function submitForm(data: OnboardingForm) {
 // No validation - what if email is invalid?
 // No password confirmation check
 // No way to provide user feedback on errors
 api.post('/onboard', data);
}

Problems:

  • No runtime validation of user input
  • No way to show validation errors
  • Password confirmation logic unenforced
  • Invalid email formats slip through

Zod + TypeScript approach (recommended)

 
import { z } from 'zod';

const PersonalInfoSchema = z.object({
 firstName: z.string().min(1, 'First name is required'),
 lastName: z.string().min(1, 'Last name is required'),
 email: z.string().email('Please enter a valid email'),
 phone: z.string().regex(/^\+?[\d\s-()]+$/, 'Invalid phone number').optional()
});

const PreferencesSchema = z.object({
 newsletter: z.boolean(),
 notifications: z.array(z.enum(['email', 'sms', 'push'])),
 theme: z.enum(['light', 'dark'])
});

const AccountSchema = z.object({
 username: z.string()
 .min(3, 'Username must be at least 3 characters')
 .regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores'),
 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"]
});

const OnboardingFormSchema = z.object({
 personalInfo: PersonalInfoSchema,
 preferences: PreferencesSchema,
 account: AccountSchema
});

type OnboardingForm = z.infer;
type PersonalInfo = z.infer;
type Preferences = z.infer;
type Account = z.infer;

function OnboardingForm() {
 const [formData, setFormData] = useState<Partial>({});
 const [errors, setErrors] = useState<Record<string, string>>({});

 const validateStep = (stepData: unknown, schema: z.ZodSchema) => {
 try {
 schema.parse(stepData);
 return true;
 } catch (error) {
 if (error instanceof z.ZodError) {
 const fieldErrors: Record<string, string> = {};
 error.errors.forEach(err => {
 const path = err.path.join('.');
 fieldErrors[path] = err.message;
 });
 setErrors(fieldErrors);
 }
 return false;
 }
 };

 const submitForm = async (data: OnboardingForm) => {
 try {
 const validatedData = OnboardingFormSchema.parse(data);
 await api.post('/onboard', validatedData);

 // handle success
 } catch (error) {
 if (error instanceof z.ZodError) {
 // Show validation errors
 setErrors(/* format errors */);
 }
 }
 };

 // Component JSX with error handling...
}

Why this is better:

  • Real-time feedback for users
  • Type-safe form data handling
  • Complex rules like password confirmation
  • Automatic TypeScript inference
  • Reusable, composable schemas

The tradeoffs

TypeScript-only advantages

  • Simpler mental model
  • Faster for small internal projects
  • No runtime cost

Zod + TypeScript advantages

  • Runtime safety with rich feedback
  • Complex validation logic support
  • Better user and developer experience

When to choose each

  • Simple internal forms: TypeScript only
  • User-facing forms: Zod + TypeScript
  • External data: Always Zod + TypeScript

Best practices and takeaways

When to use TypeScript only

  • Internal utilities and business logic
  • Component props and controlled state
  • Trusted configuration objects

When to use Zod only

  • One-off validation scripts
  • Quick prototyping
  • Runtime-only configs

When to use Both

  • API request/response handling
  • User input and form validation
  • External data ingestion
  • Config files that affect app behavior

Pro Tips

  1. Start with Zod schemas, then infer TypeScript types.
  2. Use transform() to reshape data, not just validate it.
  3. Validate early โ€” at system entry points.
  4. Cache parsed data to reduce overhead.
  5. Reuse schemas across client and server when possible.

Conclusion

Choosing between TypeScript, Zod, or both isnโ€™t about competition โ€” itโ€™s about coverage. TypeScript gives you confidence in how your code runs, while Zod ensures the data your code touches is safe and valid.

P.S. Validate trust boundaries, type-check everything else. Your users (and your future self) will thank you.

LogRocket understands everything users do in your web and mobile apps.

๐Ÿ‘ LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings โ€” compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

Modernize how you understand your web and mobile apps โ€” start monitoring for free.

๐Ÿ‘ Image
๐Ÿ‘ Image
๐Ÿ‘ Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo โ€” with email/password login, Google OAuth, session persistence, and protected routes.

๐Ÿ‘ Image
Chinwike Maduabuchi
Jun 9, 2026 โ‹… 13 min read

AI dev tool power rankings & comparison [June 2026]

Compare the top AI development tools and models of June 2026. View updated rankings, feature breakdowns, and find the best fit for you.

๐Ÿ‘ Image
Chizaram Ken
Jun 8, 2026 โ‹… 11 min read

How to check username availability at scale with Bloom filters

Learn how Bloom filters reduce database lookups for username availability checks while preserving correctness at scale.

๐Ÿ‘ Image
Rosario De Chiara
Jun 8, 2026 โ‹… 6 min read

An advanced guide to Nuxt testing and mocking

Learn how to test Nuxt apps with Vitest, @nuxt/test-utils, runtime mocks, server route mocks, and Playwright e2e tests.

๐Ÿ‘ Image
Sebastian Weber
Jun 5, 2026 โ‹… 15 min read
View all posts

Hey there, want to help make our blog better?

Join LogRocketโ€™s Content Advisory Board. Youโ€™ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now