114 lines
3.3 KiB
Markdown
114 lines
3.3 KiB
Markdown
---
|
|
title: Use pipe() for Multi-Stage Validation
|
|
impact: MEDIUM
|
|
impactDescription: Chaining transforms loses intermediate type info; pipe() explicitly shows data flow through validation stages
|
|
tags: compose, pipe, pipeline, transform
|
|
---
|
|
|
|
## Use pipe() for Multi-Stage Validation
|
|
|
|
When data needs to pass through multiple validation stages (coerce string to number, then validate range, then transform to currency), use `.pipe()` to chain schemas. This makes the data transformation pipeline explicit and each stage's type clear.
|
|
|
|
**Incorrect (unclear transformation chain):**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
// All transforms in one long chain - hard to understand stages
|
|
const priceSchema = z
|
|
.string()
|
|
.transform((s) => parseFloat(s.replace(/[$,]/g, '')))
|
|
.refine((n) => !isNaN(n), 'Invalid number')
|
|
.refine((n) => n >= 0, 'Must be positive')
|
|
.refine((n) => n <= 1000000, 'Too large')
|
|
.transform((n) => Math.round(n * 100))
|
|
|
|
// What type is n at each stage? Hard to tell
|
|
```
|
|
|
|
**Correct (using pipe for clear stages):**
|
|
|
|
```typescript
|
|
import { z } from 'zod'
|
|
|
|
// Stage 1: Coerce string to number
|
|
const parsePrice = z.string().transform((s) => {
|
|
const cleaned = s.replace(/[$,]/g, '')
|
|
const parsed = parseFloat(cleaned)
|
|
if (isNaN(parsed)) throw new Error('Invalid number')
|
|
return parsed
|
|
})
|
|
|
|
// Stage 2: Validate number constraints
|
|
const validPrice = z.number().min(0, 'Must be positive').max(1000000, 'Too large')
|
|
|
|
// Stage 3: Transform to cents
|
|
const centsPrice = z.number().transform((n) => Math.round(n * 100))
|
|
|
|
// Pipe them together - clear data flow
|
|
const priceSchema = parsePrice.pipe(validPrice).pipe(centsPrice)
|
|
|
|
// Type at each stage is clear:
|
|
// string -> number (parsePrice)
|
|
// number -> number (validPrice)
|
|
// number -> number (centsPrice, but semantically cents)
|
|
```
|
|
|
|
**Coercion with validation:**
|
|
|
|
```typescript
|
|
// Without pipe - validation runs on raw input
|
|
const schema1 = z.coerce.number().min(1)
|
|
schema1.parse('') // Passes! Empty string coerces to 0, but then... wait, 0 < 1
|
|
|
|
// With pipe - validation runs on coerced value
|
|
const schema2 = z.coerce.number().pipe(z.number().min(1))
|
|
schema2.parse('') // Fails correctly: 0 is less than 1
|
|
```
|
|
|
|
**Complex data transformation:**
|
|
|
|
```typescript
|
|
// Input: CSV string of emails
|
|
// Output: Array of normalized, validated email objects
|
|
|
|
const emailArraySchema = z
|
|
.string()
|
|
// Stage 1: Split CSV
|
|
.transform((s) => s.split(',').map((e) => e.trim()))
|
|
// Stage 2: Validate as email array
|
|
.pipe(z.array(z.string().email()))
|
|
// Stage 3: Transform to objects
|
|
.pipe(
|
|
z.array(z.string()).transform((emails) =>
|
|
emails.map((email) => ({
|
|
address: email.toLowerCase(),
|
|
domain: email.split('@')[1],
|
|
}))
|
|
)
|
|
)
|
|
|
|
emailArraySchema.parse('John@Example.com, jane@test.com')
|
|
// [
|
|
// { address: 'john@example.com', domain: 'Example.com' },
|
|
// { address: 'jane@test.com', domain: 'test.com' }
|
|
// ]
|
|
```
|
|
|
|
**Type inference with pipe:**
|
|
|
|
```typescript
|
|
const schema = z.string().pipe(z.coerce.number()).pipe(z.number().positive())
|
|
|
|
type Input = z.input<typeof schema> // string
|
|
type Output = z.output<typeof schema> // number
|
|
|
|
// Each pipe stage has clear input/output types
|
|
```
|
|
|
|
**When NOT to use this pattern:**
|
|
- Simple single-stage validation (adds unnecessary complexity)
|
|
- When `.refine()` chain is sufficient and readable
|
|
|
|
Reference: [Zod API - pipe](https://zod.dev/api#pipe)
|