3.4 KiB
3.4 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Distinguish transform() from refine() and coerce() | MEDIUM | Using wrong method causes validation to pass with wrong data; each method has distinct purpose | refine, transform, coerce, conversion |
Distinguish transform() from refine() and coerce()
.refine() validates and returns boolean, .transform() converts data to new format, and .coerce converts input before validation. Using the wrong one causes bugs where validation passes but data is wrong.
Purpose of each method:
import { z } from 'zod'
// coerce: Convert type BEFORE validation
// Input: unknown -> Output: validated type
z.coerce.number().parse('42') // Converts "42" to 42, then validates as number
// refine: Validate with custom logic, return boolean
// Input: T -> Output: T (unchanged, but validated)
z.number().refine((n) => n > 0) // Validates n > 0, returns n unchanged
// transform: Convert to different type AFTER validation
// Input: T -> Output: U (different type)
z.string().transform((s) => s.length) // Validates string, returns length
Incorrect (using transform for validation):
// Wrong: transform should convert, not validate
const schema = z.number().transform((n) => {
if (n < 0) throw new Error('Must be positive') // Don't throw in transform
return n
})
Correct (using appropriate method):
import { z } from 'zod'
// VALIDATION: Use refine - returns boolean, data unchanged
const positiveNumber = z.number().refine(
(n) => n > 0,
{ message: 'Must be positive' }
)
positiveNumber.parse(5) // 5
positiveNumber.parse(-1) // ZodError: Must be positive
// CONVERSION: Use transform - returns new value
const stringLength = z.string().transform((s) => s.length)
type StringLength = z.infer<typeof stringLength> // number
stringLength.parse('hello') // 5
// COERCION: Use coerce - converts input type
const coercedNumber = z.coerce.number()
coercedNumber.parse('42') // 42 (from string)
coercedNumber.parse(42) // 42 (already number)
Combining methods correctly:
// Input: string -> Coerce to number -> Validate positive -> Transform to dollars
const priceSchema = z.coerce
.number()
.refine((n) => n >= 0, 'Price cannot be negative')
.transform((cents) => `$${(cents / 100).toFixed(2)}`)
priceSchema.parse('1999') // "$19.99"
priceSchema.parse('-100') // ZodError: Price cannot be negative
Order of operations:
const schema = z
.preprocess(val => val, z.string()) // 1. preprocess (before type check)
.transform(s => s.trim()) // 2. transform (after type check)
.refine(s => s.length > 0) // 3. refine (custom validation)
.transform(s => s.toUpperCase()) // 4. another transform
// Input flows: preprocess -> type check -> transforms/refines in order
Use case comparison:
| Need | Method | Example |
|---|---|---|
| Convert string to number | z.coerce.number() |
Form input |
| Validate number is positive | .refine(n => n > 0) |
Business rule |
| Convert cents to dollars | .transform(n => n/100) |
Display format |
| Trim whitespace before check | z.preprocess |
Input cleanup |
When NOT to use this pattern:
- Simple type coercion: use
z.coerce.* - Simple validation: use built-in methods like
.min(),.email()
Reference: Zod API - transform