DEV Community

Cover image for Why You Should Use Zod Instead of TypeScript Alone
Roman
Roman

Posted on

Why You Should Use Zod Instead of TypeScript Alone

Have you ever encountered a situation where your TypeScript-powered application, despite having perfect type definitions, unexpectedly broke at runtime due to an unexpected API change or malformed user input? You're not alone. While TypeScript significantly improves developer experience by catching type errors during compile time, it does not protect you at runtime. TypeScript doesn't fix your data - it just makes the problems easier to see. But in order to simplify our lives and protect at runtime, we can combine it with zod (typescript + zod = ❤️).

What is Zod?

From their website:
”TypeScript-first schema validation with static type inference by @colinhacks

Unlike TypeScript, which checks types only at compile-time, Zod ensures data adheres to defined schemas at runtime, preventing unexpected errors or misleading data.

Additionally, Zod offers various integrations with many tools, starting with simple schema validations, to complex forms, APIs, and database schemas validations, which helps you to protect your schema and data type at all stages of development.

Why TypeScript Alone Isn’t Enough

Yes, I know, it won’t be new for your or it won’t surprise you. TypeScript offers type safety only during build/compile phase. It still helps reduce errors and improve documentation — so even when revisiting old code, you can make a fairly confident guess about what it’s doing 🙂. However, at runtime, we’re back to plain JavaScript—dynamic, flexible, but without any built-in type safety, making our apps vulnerable to unexpected errors.

Here is quick example:

interface Profile {
 id: number
 name: string
 description: string
 imageSrc: string
}

const getProfile = (): Promise<Profile> => {
 return fetch('/api/profile')
  .then(res => res.json()) // Generally there is no guarantee that it will match the interface.
} 

const profile = await getProfile()
Enter fullscreen mode Exit fullscreen mode

How Zod Complements TypeScript

Zod provides explicit runtime validation, which allows to make sure that your data’s shape is still what you expect and avoid surprising errors:

import { z } from 'zod'

const ProfileSchema = z.object({
 id: z.number(),
 name: z.string(),
 description: z.string(),
 imageSrc: z.string()
})

type Profile = z.infer<typeof ProfileSchema>

const getProfile = (): Promise<Profile> => {
 return fetch('/api/profile')
  .then(res => res.json())
  .then(profile => {
      return ProfileSchema.parse(profile) // Throws an error if validation failed.
  })
} 

const profile = await getProfile()

Enter fullscreen mode Exit fullscreen mode

So with Zod’s usage we have:

  • Less boilerplate - fewer manual checks, cleaner code
  • Automatic type inference - instantly reflects schema updates
  • Easy to maintain and scale - centralized schema definitions
  • Simplified advanced error handling - clearer error outputs for debugging
  • Integration across your entire stack - from forms to APIs to database interactions

In real world you can integrate it with pretty much anything:

  • Form validation: e.g. integrate with React Hook Forms for validation
  • API Routes: validate incoming requests and outgoing response formats
  • External API integrations: check response structure from services like supabase, or stripe, .etc

API Example:

export async function POST(req: Request) {
 const body = await req.json()
 const result = BodySchema.safeParse(body)

 if (!result.success) {
  return Response.json({ errors: result.error.format() }, { status: 400 })
 }

 const requestData = result.data
 // at this moment we can safely proceed with valid request data
}
Enter fullscreen mode Exit fullscreen mode

React Forms Example:

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'

const formSchema = z.object({ email: z.string().email() })

function MyForm() {
  const { register, handleSubmit, formState } = useForm({
    resolver: zodResolver(formSchema)
  })

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <input {...register('email')} />
      {formState.errors.email && <span>{formState.errors.email.message}</span>}
      <button type='submit'>Submit</button>
    </form>
  )
}
Enter fullscreen mode Exit fullscreen mode

Performance Impact

Overall Zod adds minimal overhead - practically unnoticeable in most real-world applications,
its simplicity and reliability easily justify the minimal performance cost — but keep in mind your specific use case, especially when dealing with deeply nested or large JSON structures.

Conclusion

So combining both Zod and TypeScript provides compile-time and runtime safety, enhancing reliability and confidence of the application and us as developers. If you're looking for a way to solve runtime safety issues, Zod is an excellent solution.

Links & Resources

Top comments (0)