106 lines
3.4 KiB
Markdown
106 lines
3.4 KiB
Markdown
---
|
|
title: Distinguish transform() from refine() and coerce()
|
|
impact: MEDIUM
|
|
impactDescription: Using wrong method causes validation to pass with wrong data; each method has distinct purpose
|
|
tags: 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:**
|
|
|
|
```typescript
|
|
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):**
|
|
|
|
```typescript
|
|
// 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):**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
// 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:**
|
|
|
|
```typescript
|
|
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](https://zod.dev/api#transform)
|