Added AI skills
This commit is contained in:
141
.agents/skills/zod/references/refine-add-path.md
Normal file
141
.agents/skills/zod/references/refine-add-path.md
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: Add Path to Refinement Errors
|
||||
impact: MEDIUM
|
||||
impactDescription: Errors without path show at object level; adding path highlights the specific field that failed
|
||||
tags: refine, path, errors, forms
|
||||
---
|
||||
|
||||
## Add Path to Refinement Errors
|
||||
|
||||
When using `.refine()` on object schemas for cross-field validation, add a `path` option to indicate which field the error relates to. Without it, the error appears at the object level, making form error display confusing.
|
||||
|
||||
**Incorrect (error at object level):**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
const formSchema = z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string(),
|
||||
}).refine(
|
||||
(data) => data.password === data.confirmPassword,
|
||||
{ message: 'Passwords do not match' } // No path specified
|
||||
)
|
||||
|
||||
const result = formSchema.safeParse({
|
||||
password: 'secret123',
|
||||
confirmPassword: 'different',
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
const flattened = result.error.flatten()
|
||||
// {
|
||||
// formErrors: ['Passwords do not match'], // At form level!
|
||||
// fieldErrors: {} // Empty - no field association
|
||||
// }
|
||||
}
|
||||
|
||||
// Form UI can't highlight which field has the error
|
||||
```
|
||||
|
||||
**Correct (error with path):**
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
const formSchema = z.object({
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string(),
|
||||
}).refine(
|
||||
(data) => data.password === data.confirmPassword,
|
||||
{
|
||||
message: 'Passwords do not match',
|
||||
path: ['confirmPassword'], // Error appears on this field
|
||||
}
|
||||
)
|
||||
|
||||
const result = formSchema.safeParse({
|
||||
password: 'secret123',
|
||||
confirmPassword: 'different',
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
const flattened = result.error.flatten()
|
||||
// {
|
||||
// formErrors: [],
|
||||
// fieldErrors: {
|
||||
// confirmPassword: ['Passwords do not match'] // Associated with field
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// Form can now show error next to confirmPassword input
|
||||
```
|
||||
|
||||
**Multiple cross-field validations:**
|
||||
|
||||
```typescript
|
||||
const dateRangeSchema = z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
minDays: z.number().optional(),
|
||||
maxDays: z.number().optional(),
|
||||
}).refine(
|
||||
(data) => data.endDate >= data.startDate,
|
||||
{ message: 'End date must be after start date', path: ['endDate'] }
|
||||
).refine(
|
||||
(data) => {
|
||||
if (!data.minDays) return true
|
||||
const days = (data.endDate.getTime() - data.startDate.getTime()) / 86400000
|
||||
return days >= data.minDays
|
||||
},
|
||||
{ message: 'Date range is too short', path: ['endDate'] }
|
||||
).refine(
|
||||
(data) => {
|
||||
if (!data.maxDays) return true
|
||||
const days = (data.endDate.getTime() - data.startDate.getTime()) / 86400000
|
||||
return days <= data.maxDays
|
||||
},
|
||||
{ message: 'Date range is too long', path: ['endDate'] }
|
||||
)
|
||||
```
|
||||
|
||||
**With superRefine for multiple path errors:**
|
||||
|
||||
```typescript
|
||||
const orderSchema = z.object({
|
||||
billingAddress: z.object({
|
||||
street: z.string(),
|
||||
city: z.string(),
|
||||
}),
|
||||
shippingAddress: z.object({
|
||||
street: z.string(),
|
||||
city: z.string(),
|
||||
}),
|
||||
sameAsBilling: z.boolean(),
|
||||
}).superRefine((data, ctx) => {
|
||||
if (data.sameAsBilling) {
|
||||
// If sameAsBilling but addresses differ, show errors on shipping
|
||||
if (data.shippingAddress.street !== data.billingAddress.street) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Must match billing address',
|
||||
path: ['shippingAddress', 'street'], // Nested path
|
||||
})
|
||||
}
|
||||
if (data.shippingAddress.city !== data.billingAddress.city) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: 'Must match billing address',
|
||||
path: ['shippingAddress', 'city'],
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**When NOT to use this pattern:**
|
||||
- When the error genuinely applies to the whole object
|
||||
- Simple single-field refinements (path is implicit)
|
||||
|
||||
Reference: [Zod API - refine](https://zod.dev/api#refine)
|
||||
Reference in New Issue
Block a user