Parsing Discriminated Unions with Zod
I've been using Zod for TypeScript validation for years, but recently I discovered something I hadn't noticed before: discriminated union validators. While looking up recursive types with z.lazy
in the docs, I came across the z.discriminatedUnion
helper. It's nice to have runtime validation for discriminated unions in TypeScript.
Discriminated Unions in TypeScript
A discriminated union in TypeScript is a way to define types that can have different shapes, where one specific field tells us which shape we're dealing with. Let's look at an example:
Looking at this type, the status field acts as our discriminator - it tells TypeScript which properties to expect. When the status is "success"
, we need a transaction ID and amount. For "failed"
status, we expect an error code and message.
You'll often see this pattern when dealing with API responses, where you might get different data structures based on how things went. Think about fetching data - you either get what you asked for, or you get error details. Each case needs its own set of properties.
TypeScript is smart enough to figure out which properties you can use just by checking that status
field. Sometimes that's all you need, but if you're dealing with external data and need runtime validation, Zod can help with that.
Validating at Runtime
While TypeScript helps us catch type errors during development, those checks disappear when our code runs in production - when the application is actually being used.
Without a validation library, checking these types at runtime typically requires verbose type guards:
This approach has several drawbacks:
- It's verbose and requires careful attention to edge cases
- It's easy to miss properties or type checks
- You need to maintain it alongside your type definitions
Using Zod's Solution
The discriminatedUnion
function in Zod handles this validation for us. It takes two arguments: the name of our discriminator field (in our case "status"
), and an array of possible object shapes.
Each shape is defined using Zod's schema builders:
When we call parse on our schema, Zod first checks the "status"
field to determine which shape to validate against, then ensures all required fields are present and have the correct types. If anything is wrong, it throws an error with detailed information about what failed.
Getting TypeScript Types from Our Schema
Zod makes it straightforward to get TypeScript types from our schema using the infer
helper:
With this approach, you get development-time type checking through TypeScript and runtime validation when you need it. The schema definition serves both purposes without extra overhead!