# How to Use Zod Union and Enum Types: A Complete Guide

> Master Zod union and enum types to validate against multiple shapes or literal values. Get runtime validation and full TypeScript inference with this complete guide.

- Repository: [Colin McDonnell/zod](https://github.com/colinhacks/zod)
- Tags: how-to-guide
- Published: 2026-02-23

---

**Zod union and enum types are first-class schema objects that validate values against multiple possible shapes or specific literal values, with full TypeScript type inference and runtime validation.**

The `colinhacks/zod` library provides powerful primitives for modeling polymorphic data and fixed sets of constants. Understanding how to leverage `z.union()` and `z.enum()` allows you to build type-safe schemas that accurately represent complex domain constraints while maintaining excellent developer experience through autocomplete and compile-time checking.

## Understanding Zod Union Types

Union types in Zod represent values that can match one of several possible schemas. Internally, these are implemented as `ZodUnion<T>` classes that extend the base `ZodType` and maintain a runtime definition object while preserving exact compile-time types.

### How Union Schemas Work Internally

According to the Zod source code, the union implementation spans both the core and classic API layers. In [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts) (lines 2063-2121), the `$ZodUnion` class defines the runtime behavior, including the `handleUnionResults` helper that merges error information from all branches when no option matches. The classic API exposes this through `ZodUnion<T>` in [`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts) (lines 1327-1344), providing the `z.union([...])` factory function.

When parsing, Zod iterates through the option schemas and short-circuits on the first successful parse. If all branches fail, the error messages are aggregated to provide a helpful diagnostic.

### Creating Simple Unions with z.union()

The `z.union()` helper accepts an array of schemas and returns a union type that validates against any one of them:

```typescript
import { z } from "zod";

// Union of string or number
const StringOrNumber = z.union([z.string(), z.number()]);

// Both parse successfully
StringOrNumber.parse("hello"); // "hello"
StringOrNumber.parse(42);      // 42

// Boolean fails with aggregated error
try {
  StringOrNumber.parse(true);
} catch (e) {
  console.error(e.errors); // Shows failures from both branches
}

```

### Optimizing with Discriminated Unions

For object unions with a shared discriminator property, use `z.discriminatedUnion()`. This builds on the same `$ZodUnion` internals but adds a fast lookup table for the discriminator key, improving performance and error specificity:

```typescript
import { z } from "zod";

const Circle = z.object({
  kind: z.literal("circle"),
  radius: z.number(),
});

const Square = z.object({
  kind: z.literal("square"),
  side: z.number(),
});

const Shape = z.discriminatedUnion("kind", [Circle, Square]);

// Validates only the matching branch
Shape.parse({ kind: "circle", radius: 10 });

// Provides specific error for the selected branch
try {
  Shape.parse({ kind: "square", radius: 5 }); // Wrong property for square
} catch (e) {
  console.error(e.errors); // Indicates "side" is required
}

```

## Working with Zod Enum Types

Zod provides two primary ways to model enumerated values: string literal enums for ad-hoc constants, and native enum support for existing TypeScript enums.

### String Literal Enums with z.enum()

The `z.enum()` function creates a schema that validates against a fixed set of string literals. Internally, this constructs a `ZodEnum` class (defined in [`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts) lines 1617-1682) backed by the core `$ZodEnum` implementation (lines 3051-3079 in [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts)):

```typescript
import { z } from "zod";

const Color = z.enum(["red", "green", "blue"]);

// Type is inferred as "red" | "green" | "blue"
type Color = z.infer<typeof Color>;

Color.parse("red");     // succeeds
Color.parse("yellow");  // throws ZodError

```

### Native TypeScript Enums with z.nativeEnum()

For existing TypeScript enums, use `z.nativeEnum()` to lift the native enum into a Zod schema while preserving both runtime values and compile-time types:

```typescript
import { z } from "zod";

enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

const DirectionSchema = z.nativeEnum(Direction);

DirectionSchema.parse("UP");       // succeeds
DirectionSchema.parse("FORWARD"); // throws ZodError

```

This helper reads the enum's entries and constructs the appropriate validation logic in the core layer, ensuring that numeric enums, string enums, and mixed enums are all handled correctly.

### Extracting Keys with z.keyof()

The `z.keyof()` utility demonstrates how enums serve as building blocks for other schemas. It extracts the keys of a `ZodObject` and returns a `ZodEnum`:

```typescript
import { z } from "zod";

const Person = z.object({
  name: z.string(),
  age: z.number(),
});

const PersonKeys = z.keyof(Person);
// Equivalent to z.enum(["name", "age"])

type Keys = z.infer<typeof PersonKeys>; // "name" | "age"

```

This implementation reads `Object.keys` from the object's output type and feeds them into the enum constructor, showcasing the composability of Zod's type system.

## Practical Code Examples

### Example 1: API Response Union

Model an API that returns either a success object or an error object:

```typescript
import { z } from "zod";

const SuccessResponse = z.object({
  status: z.literal("success"),
  data: z.object({ id: z.number() }),
});

const ErrorResponse = z.object({
  status: z.literal("error"),
  message: z.string(),
});

const ApiResponse = z.discriminatedUnion("status", [
  SuccessResponse,
  ErrorResponse,
]);

// Type narrowing works automatically
const result = ApiResponse.parse({ status: "success", data: { id: 1 } });
if (result.status === "success") {
  console.log(result.data.id); // TypeScript knows data exists
}

```

### Example 2: Configuration Enum with Native Enum

Validate configuration values against a TypeScript enum:

```typescript
import { z } from "zod";

enum LogLevel {
  DEBUG = 0,
  INFO = 1,
  WARN = 2,
  ERROR = 3,
}

const ConfigSchema = z.object({
  level: z.nativeEnum(LogLevel),
  path: z.string(),
});

const config = ConfigSchema.parse({ level: LogLevel.INFO, path: "/var/log" });
// or
ConfigSchema.parse({ level: 1, path: "/var/log" }); // Also valid for numeric enums

```

### Example 3: Heterogeneous Union with Type Guards

Handle unions of completely different primitives:

```typescript
import { z } from "zod";

const Primitive = z.union([
  z.string(),
  z.number(),
  z.boolean(),
  z.null(),
]);

type Primitive = z.infer<typeof Primitive>; // string | number | boolean | null

function processValue(val: Primitive) {
  if (typeof val === "string") {
    return val.toUpperCase();
  }
  if (typeof val === "number") {
    return val * 2;
  }
  return val;
}

```

## Summary

- **Zod union types** allow values to match one of several schemas, implemented internally by the `ZodUnion` class in [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts) and exposed via `z.union()`.
- **Discriminated unions** optimize object unions with a shared key using `z.discriminatedUnion()`, providing faster validation and clearer error messages through discriminator lookup tables.
- **String literal enums** created with `z.enum()` validate against fixed string constants, with the `ZodEnum` class defined in [`packages/zod/src/v4/classic/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/classic/schemas.ts) (lines 1617-1682).
- **Native TypeScript enums** integrate seamlessly using `z.nativeEnum()`, preserving both runtime values and compile-time type safety for numeric, string, or mixed enums.
- Both union and enum schemas propagate exact type information through generic parameters, enabling exhaustive switch statements and IDE autocompletion without manual type assertions.

## Frequently Asked Questions

### What is the difference between z.union() and z.discriminatedUnion() in Zod?

**`z.union()`** validates input against each schema option sequentially until one succeeds, making it ideal for heterogeneous types or primitives. **`z.discriminatedUnion()`** requires a discriminator key (like `kind` or `type`) and uses a lookup table to jump directly to the matching schema, offering better performance and more specific error messages for object unions. Both are implemented in [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts), but discriminated unions add a fast-path optimization for tagged objects.

### How do I extract the TypeScript type from a Zod enum or union?

Use the `z.infer<typeof schema>` utility to extract the static TypeScript type. For enums created with `z.enum(["a", "b"])`, the inferred type is the union of string literals `"a" | "b"`. For unions like `z.union([z.string(), z.number()])`, the inferred type is `string | number`. This works because both `ZodUnion` and `ZodEnum` propagate their generic type parameters through to the base `ZodType`, preserving exact type information at compile time.

### Can I use numeric enums with z.nativeEnum()?

Yes, `z.nativeEnum()` fully supports numeric enums, string enums, and mixed enums. When you pass a TypeScript numeric enum like `enum Level { Low = 1, High = 2 }`, Zod validates that the input matches any of the enum's numeric values (1 or 2). The implementation in [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts) reads the enum's entries to build the validation logic, ensuring both runtime safety and TypeScript type narrowing work correctly for numeric variants.

### What happens when a Zod union validation fails?

When no branch of a union matches the input, Zod aggregates the error messages from all attempted branches into a single `ZodError`. The `handleUnionResults` helper in [`packages/zod/src/v4/core/schemas.ts`](https://github.com/colinhacks/zod/blob/main/packages/zod/src/v4/core/schemas.ts) (lines 2096-2121) merges the issues arrays from each failed option, providing a comprehensive error message that indicates why each branch failed. For discriminated unions, the error is more specific: if the discriminator value doesn't match any branch, or if the matched branch fails validation, the error points directly to the problematic property in the selected schema.