Validate String Literal Types with Zod
Problem Statement
When working with TypeScript, you may define specific string literal types like:
type PaymentType = 'CHECK' | 'DIRECT DEPOSIT' | 'MONEY ORDER';However, runtime validation of these types presents challenges. When receiving input from external sources (like APIs or user input), you need to verify that strings match one of these pre-defined options. Using Zod's generic z.string() validator won't ensure that the string matches one of your exact literal values.
Recommended Solution: z.enum with as const
The most maintainable solution uses Zod's z.enum combined with TypeScript's as const assertion. This creates a single source of truth for both runtime validation and type inference:
import { z } from 'zod';
// Define values with 'as const' for tuple inference
const PAYMENT_TYPES = ['CHECK', 'DIRECT DEPOSIT', 'MONEY ORDER'] as const;
// Create Zod schema
const PaymentTypeSchema = z.enum(PAYMENT_TYPES);
// Infer TypeScript type from schema
type PaymentType = z.infer<typeof PaymentTypeSchema>;
// Use in your schema definition
const schema = z.object({
paymentType: PaymentTypeSchema,
});
// Valid usage
schema.parse({ paymentType: 'CHECK' }); // ✓ Passes
// Invalid usage
schema.parse({ paymentType: 'CASH' });
// ❌ Throws error: "Invalid enum value. Expected 'CHECK' | 'DIRECT DEPOSIT' | 'MONEY ORDER'"Key Advantages
- Single Source of Truth: The
PAYMENT_TYPESarray defines values once for both runtime and types - Type Safety:
as constpreserves literal types (instead of converting tostring[]) - DRY Principle: Avoids duplicating value definitions
- Type Inference:
z.inferautomatically aligns your types with Zod's validation - Extensibility: Add/remove values in one location
TIP
Use this method when you need to co-locate runtime values with TypeScript types without duplication
Alternative Approaches
Union Type with z.union
For more control or custom error messages per literal:
const PaymentTypeSchema = z.union([
z.literal('CHECK'),
z.literal('DIRECT DEPOSIT'),
z.literal('MONEY ORDER'),
]);Note
This becomes verbose quickly for more than 3-4 options. Prefer z.enum for simple cases
Native TypeScript Enums
If you prefer traditional enums:
enum PaymentType {
Check = 'CHECK',
DirectDeposit = 'DIRECT DEPOSIT',
MoneyOrder = 'MONEY ORDER'
}
const PaymentTypeSchema = z.nativeEnum(PaymentType);Enum Limitations
TypeScript enums have several drawbacks:
- Create runtime code in compiled JS
- Less flexible with dynamic operations
- Mix numeric/string values can cause confusion
When using z.nativeEnum, avoid numerical enums as they behave differently at runtime.
Implementation Best Practices
1. Defining Literal Arrays Properly
Always use as const with your literal arrays:
// Correct
const COLORS = ['RED', 'GREEN', 'BLUE'] as const;
// Incorrect (loses literal type information)
const COLORS = ['RED', 'GREEN', 'BLUE'];2. Exporting Schemas and Types
Organize your schemas for reuse:
// schemas.ts
export const PAYMENT_TYPES = [...] as const;
export const PaymentTypeSchema = z.enum(PAYMENT_TYPES);
export type PaymentType = z.infer<typeof PaymentTypeSchema>;
// usage.ts
import { PaymentTypeSchema, type PaymentType } from './schemas';3. Handling Null/Undefined
For optional values, combine with .optional() or .nullable():
const schema = z.object({
paymentType: PaymentTypeSchema.optional(),
});Common Errors and Solutions
"Type 'string' is not assignable to type..."
Problem: Assigning strings to functions expecting specific literals
const setPayment = (type: PaymentType) => {}
// ❌ Error: string is not assignable to PaymentType
const payment = 'CHECK';
setPayment(payment);Solution: Validate strings with your schema:
// Before use, parse the value
const getPaymentType = (input: string) => {
return PaymentTypeSchema.parse(input); // Returns PaymentType
}Enum Schemas Changing at Runtime
Problem: Arrays without as const widen to string[]
const PAYMENTS = ['CHECK', 'DIRECT DEPOSIT']; // Inferred as string[]
PAYMENTS.push('INVALID'); // 💥 Runtime array corruptionSolution: Use readonly arrays with as const:
const PAYMENTS = ['CHECK', 'DIRECT DEPOSIT'] as const;
// ❌ Now errors at compile-time if modified:
PAYMENTS.push('INVALID');Choosing the Right Approach
| Method | Best For | When to Avoid |
|---|---|---|
z.enum + as const | Most common cases | Complex validation logic |
z.union + z.literal | Custom per-value logic | Large option sets (>5 values) |
z.nativeEnum | Legacy code with existing enums | New projects, dynamic use cases |
For most applications, the z.enum with as const approach provides the best combination of type safety, runtime validation, and maintainability.