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()
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()
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
}
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>
)
}
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.
Top comments (0)