@kood/claude-code 0.3.9 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/tanstack-start/docs/library/better-auth/index.md +225 -185
- package/templates/tanstack-start/docs/library/prisma/index.md +1025 -41
- package/templates/tanstack-start/docs/library/t3-env/index.md +207 -40
- package/templates/tanstack-start/docs/library/tanstack-query/index.md +878 -42
- package/templates/tanstack-start/docs/library/tanstack-router/index.md +602 -54
- package/templates/tanstack-start/docs/library/tanstack-start/index.md +1334 -33
- package/templates/tanstack-start/docs/library/zod/index.md +674 -31
|
@@ -2,55 +2,698 @@
|
|
|
2
2
|
|
|
3
3
|
> v4 | TypeScript Schema Validation
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
<context>
|
|
6
|
+
|
|
7
|
+
**Purpose:** Type-safe schema validation with TypeScript inference
|
|
8
|
+
**Requirements:** TypeScript v5.5+, strict mode enabled
|
|
9
|
+
**Bundle Size:** 2kb core (gzipped), zero dependencies
|
|
10
|
+
|
|
11
|
+
</context>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<forbidden>
|
|
16
|
+
|
|
17
|
+
| 분류 | 금지 |
|
|
18
|
+
|------|------|
|
|
19
|
+
| **Deprecated API** | `z.string().email()`, `z.string().url()`, `z.string().uuid()` |
|
|
20
|
+
| **Error Messages** | `message`, `invalid_type_error`, `required_error` (v3) |
|
|
21
|
+
| **타입** | any 타입 (unknown 사용) |
|
|
22
|
+
|
|
23
|
+
</forbidden>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<required>
|
|
28
|
+
|
|
29
|
+
| 분류 | 필수 |
|
|
30
|
+
|------|------|
|
|
31
|
+
| **v4 API** | `z.email()`, `z.url()`, `z.uuid()` (top-level) |
|
|
32
|
+
| **Error Param** | `{ error: "message" }` (v4 통합 파라미터) |
|
|
33
|
+
| **TanStack Start** | `.inputValidator(schema)` 사용 |
|
|
34
|
+
| **타입 추론** | `z.infer<typeof schema>` |
|
|
35
|
+
|
|
36
|
+
</required>
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<installation>
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install zod@^4.0.0
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## TypeScript Config
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"compilerOptions": {
|
|
53
|
+
"strict": true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
</installation>
|
|
8
59
|
|
|
9
60
|
---
|
|
10
61
|
|
|
11
|
-
<
|
|
62
|
+
<v4_breaking_changes>
|
|
63
|
+
|
|
64
|
+
## v3 → v4 Breaking Changes
|
|
65
|
+
|
|
66
|
+
| v3 (Deprecated) | v4 (Required) |
|
|
67
|
+
|-----------------|---------------|
|
|
68
|
+
| `z.string().email()` | `z.email()` |
|
|
69
|
+
| `z.string().url()` | `z.url()` |
|
|
70
|
+
| `z.string().uuid()` | `z.uuid()` |
|
|
71
|
+
| `z.string().min(5, { message: "..." })` | `z.string().min(5, { error: "..." })` |
|
|
72
|
+
| `z.string().email({ message: "..." })` | `z.email({ error: "..." })` |
|
|
73
|
+
|
|
74
|
+
## Refinement Chaining
|
|
12
75
|
|
|
13
76
|
```typescript
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
77
|
+
// ✅ v4: Refinement 체이닝 가능
|
|
78
|
+
z.string().refine(val => val.includes("@")).min(5)
|
|
79
|
+
|
|
80
|
+
// ❌ v3: ZodEffects로 래핑되어 체이닝 불가
|
|
81
|
+
z.string().refine(val => val.includes("@")).min(5) // Error
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Object Types
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// ✅ v4: 새로운 객체 타입
|
|
88
|
+
z.strictObject({ name: z.string() }) // 추가 키 에러
|
|
89
|
+
z.looseObject({ name: z.string() }) // 추가 키 통과
|
|
90
|
+
z.object({ name: z.string() }) // 기본 동작 (strip)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
</v4_breaking_changes>
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
<v4_new_apis>
|
|
98
|
+
|
|
99
|
+
## String Formats
|
|
100
|
+
|
|
101
|
+
| API | 설명 |
|
|
102
|
+
|-----|------|
|
|
103
|
+
| `z.email()` | 이메일 검증 |
|
|
104
|
+
| `z.url()` | URL 검증 |
|
|
105
|
+
| `z.uuid()`, `z.uuidv1()`, `z.uuidv4()`, `z.uuidv7()` | UUID 검증 |
|
|
106
|
+
| `z.ipv4()`, `z.ipv6()` | IP 주소 |
|
|
107
|
+
| `z.cidr()`, `z.cidrvv4()`, `z.cidrv6()` | CIDR 표기법 |
|
|
108
|
+
| `z.base64()`, `z.base64url()` | Base64 인코딩 |
|
|
109
|
+
| `z.jwt()` | JWT 토큰 |
|
|
110
|
+
|
|
111
|
+
## ISO Formats
|
|
112
|
+
|
|
113
|
+
| API | 설명 |
|
|
114
|
+
|-----|------|
|
|
115
|
+
| `z.iso.date()` | ISO 8601 날짜 (YYYY-MM-DD) |
|
|
116
|
+
| `z.iso.datetime()` | ISO 8601 날짜/시간 |
|
|
117
|
+
| `z.iso.time()` | ISO 8601 시간 |
|
|
118
|
+
| `z.iso.duration()` | ISO 8601 기간 |
|
|
119
|
+
|
|
120
|
+
## Numeric Types
|
|
121
|
+
|
|
122
|
+
| API | 설명 |
|
|
123
|
+
|-----|------|
|
|
124
|
+
| `z.int()` | 정수 |
|
|
125
|
+
| `z.float32()`, `z.float64()` | 부동소수점 |
|
|
126
|
+
| `z.int32()`, `z.uint32()` | 32비트 정수 |
|
|
127
|
+
| `z.int64()`, `z.uint64()` | 64비트 정수 |
|
|
128
|
+
|
|
129
|
+
## Special Types
|
|
130
|
+
|
|
131
|
+
| API | 설명 | 예시 |
|
|
132
|
+
|-----|------|------|
|
|
133
|
+
| `z.stringbool()` | 문자열 → 불리언 | `"true"`, `"1"`, `"yes"` → `true` |
|
|
134
|
+
| `z.templateLiteral()` | 템플릿 리터럴 타입 | `` `${number}px` `` |
|
|
135
|
+
| `z.file()` | 파일 검증 | `.min()`, `.max()`, `.mime()` |
|
|
136
|
+
|
|
137
|
+
## Metadata & Tools
|
|
138
|
+
|
|
139
|
+
| API | 설명 |
|
|
140
|
+
|-----|------|
|
|
141
|
+
| `z.registry()` | 커스텀 메타데이터 |
|
|
142
|
+
| `z.globalRegistry` | JSON Schema 호환 메타데이터 |
|
|
143
|
+
| `.meta()` | 스키마 메타데이터 첨부 |
|
|
144
|
+
| `z.toJSONSchema()` | JSON Schema 변환 |
|
|
145
|
+
| `z.prettifyError()` | 사용자 친화적 에러 포맷 |
|
|
146
|
+
| `.overwrite()` | 타입 보존 변환 |
|
|
147
|
+
|
|
148
|
+
</v4_new_apis>
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
<basic_usage>
|
|
153
|
+
|
|
154
|
+
## Basic Schema
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
import { z } from 'zod'
|
|
158
|
+
|
|
159
|
+
// ✅ v4 API
|
|
160
|
+
const userSchema = z.object({
|
|
161
|
+
email: z.email(),
|
|
17
162
|
name: z.string().min(1).trim(),
|
|
18
|
-
website: z.url().optional(),
|
|
163
|
+
website: z.url().optional(),
|
|
19
164
|
age: z.number().int().positive(),
|
|
20
165
|
})
|
|
21
|
-
type Input = z.infer<typeof schema>
|
|
22
166
|
|
|
23
|
-
|
|
24
|
-
|
|
167
|
+
type User = z.infer<typeof userSchema>
|
|
168
|
+
// → { email: string; name: string; website?: string; age: number }
|
|
169
|
+
|
|
170
|
+
// ✅ 검증
|
|
171
|
+
const result = userSchema.parse(data) // 실패 시 throw
|
|
172
|
+
const safe = userSchema.safeParse(data) // { success, data/error }
|
|
173
|
+
|
|
174
|
+
if (safe.success) {
|
|
175
|
+
console.log(safe.data.email)
|
|
176
|
+
} else {
|
|
177
|
+
console.error(safe.error.issues)
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## TanStack Start Integration
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { createServerFn } from '@tanstack/start'
|
|
185
|
+
import { z } from 'zod'
|
|
186
|
+
|
|
187
|
+
// ✅ Server Function with Zod v4
|
|
188
|
+
const createUserSchema = z.object({
|
|
189
|
+
email: z.email(),
|
|
190
|
+
name: z.string().min(1).trim(),
|
|
191
|
+
website: z.url().optional(),
|
|
192
|
+
})
|
|
25
193
|
|
|
26
|
-
// TanStack Start
|
|
27
194
|
export const createUser = createServerFn({ method: 'POST' })
|
|
28
|
-
.inputValidator(
|
|
29
|
-
.handler(async ({ data }) =>
|
|
195
|
+
.inputValidator(createUserSchema)
|
|
196
|
+
.handler(async ({ data }) => {
|
|
197
|
+
// ^? { email: string; name: string; website?: string }
|
|
198
|
+
return prisma.user.create({ data })
|
|
199
|
+
})
|
|
30
200
|
```
|
|
31
201
|
|
|
32
|
-
</
|
|
202
|
+
</basic_usage>
|
|
33
203
|
|
|
34
|
-
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
<primitives>
|
|
207
|
+
|
|
208
|
+
## Primitive Types
|
|
209
|
+
|
|
210
|
+
| Type | Example |
|
|
211
|
+
|------|---------|
|
|
212
|
+
| `z.string()` | `z.string().min(5).max(100)` |
|
|
213
|
+
| `z.number()` | `z.number().int().positive()` |
|
|
214
|
+
| `z.boolean()` | `z.boolean()` |
|
|
215
|
+
| `z.date()` | `z.date().min(new Date('2020-01-01'))` |
|
|
216
|
+
| `z.null()` | `z.null()` |
|
|
217
|
+
| `z.undefined()` | `z.undefined()` |
|
|
218
|
+
| `z.void()` | `z.void()` |
|
|
219
|
+
| `z.any()` | `z.any()` (❌ 사용 지양) |
|
|
220
|
+
| `z.unknown()` | `z.unknown()` (✅ 권장) |
|
|
221
|
+
| `z.never()` | `z.never()` |
|
|
222
|
+
|
|
223
|
+
## String Methods
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
z.string()
|
|
227
|
+
.min(5, { error: "Too short." })
|
|
228
|
+
.max(100, { error: "Too long." })
|
|
229
|
+
.length(10, { error: "Must be 10 chars." })
|
|
230
|
+
.trim()
|
|
231
|
+
.toLowerCase()
|
|
232
|
+
.toUpperCase()
|
|
233
|
+
.startsWith("https://")
|
|
234
|
+
.endsWith(".com")
|
|
235
|
+
.regex(/^\d+$/, { error: "Must be digits." })
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Number Methods
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
z.number()
|
|
242
|
+
.gt(5) // Greater than
|
|
243
|
+
.gte(5) // Greater than or equal
|
|
244
|
+
.lt(100) // Less than
|
|
245
|
+
.lte(100) // Less than or equal
|
|
246
|
+
.int() // Integer
|
|
247
|
+
.positive() // > 0
|
|
248
|
+
.nonnegative()// >= 0
|
|
249
|
+
.negative() // < 0
|
|
250
|
+
.nonpositive()// <= 0
|
|
251
|
+
.multipleOf(5)// 배수
|
|
252
|
+
.finite() // Not Infinity/-Infinity
|
|
253
|
+
.safe() // Number.MIN_SAFE_INTEGER ~ MAX_SAFE_INTEGER
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
</primitives>
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
<complex_types>
|
|
261
|
+
|
|
262
|
+
## Objects
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const schema = z.object({
|
|
266
|
+
name: z.string(),
|
|
267
|
+
age: z.number(),
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// ✅ Methods
|
|
271
|
+
schema.shape.name // z.string()
|
|
272
|
+
schema.keyof() // z.enum(['name', 'age'])
|
|
273
|
+
schema.extend({ email: z.email() })
|
|
274
|
+
schema.merge(otherSchema)
|
|
275
|
+
schema.pick({ name: true })
|
|
276
|
+
schema.omit({ age: true })
|
|
277
|
+
schema.partial() // 모든 필드 optional
|
|
278
|
+
schema.deepPartial() // 재귀적 partial
|
|
279
|
+
schema.required() // 모든 필드 required
|
|
280
|
+
schema.passthrough() // 추가 키 허용
|
|
281
|
+
schema.strict() // 추가 키 에러
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Arrays
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
z.array(z.string())
|
|
288
|
+
.min(1, { error: "At least 1 item." })
|
|
289
|
+
.max(10)
|
|
290
|
+
.length(5)
|
|
291
|
+
.nonempty()
|
|
292
|
+
|
|
293
|
+
// ✅ Non-empty array
|
|
294
|
+
z.string().array().nonempty()
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Tuples
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
const tuple = z.tuple([
|
|
301
|
+
z.string(),
|
|
302
|
+
z.number(),
|
|
303
|
+
z.boolean(),
|
|
304
|
+
])
|
|
305
|
+
|
|
306
|
+
type Tuple = z.infer<typeof tuple>
|
|
307
|
+
// → [string, number, boolean]
|
|
308
|
+
|
|
309
|
+
// ✅ Rest parameter
|
|
310
|
+
z.tuple([z.string()]).rest(z.number())
|
|
311
|
+
// → [string, ...number[]]
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Unions
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
const schema = z.union([z.string(), z.number()])
|
|
318
|
+
// 또는
|
|
319
|
+
const schema = z.string().or(z.number())
|
|
320
|
+
|
|
321
|
+
type Value = z.infer<typeof schema>
|
|
322
|
+
// → string | number
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Discriminated Unions
|
|
35
326
|
|
|
36
327
|
```typescript
|
|
37
|
-
|
|
38
|
-
z.
|
|
39
|
-
z.
|
|
40
|
-
|
|
328
|
+
const schema = z.discriminatedUnion('type', [
|
|
329
|
+
z.object({ type: z.literal('email'), email: z.email() }),
|
|
330
|
+
z.object({ type: z.literal('phone'), phone: z.string() }),
|
|
331
|
+
])
|
|
332
|
+
|
|
333
|
+
type Contact = z.infer<typeof schema>
|
|
334
|
+
// → { type: 'email'; email: string } | { type: 'phone'; phone: string }
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
## Records
|
|
41
338
|
|
|
42
|
-
|
|
43
|
-
z.
|
|
339
|
+
```typescript
|
|
340
|
+
z.record(z.string()) // Record<string, string>
|
|
341
|
+
z.record(z.enum(['a', 'b']), z.number()) // Record<'a' | 'b', number>
|
|
342
|
+
```
|
|
44
343
|
|
|
45
|
-
|
|
46
|
-
z.string().min(5, { error: "Too short." }) // message → error
|
|
47
|
-
z.strictObject({ name: z.string() }) // Error on extra keys
|
|
48
|
-
z.looseObject({ name: z.string() }) // Allow extra keys
|
|
49
|
-
z.string().refine(val => val.includes("@")).min(5) // Refinement chaining
|
|
344
|
+
## Maps & Sets
|
|
50
345
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
346
|
+
```typescript
|
|
347
|
+
z.map(z.string(), z.number()) // Map<string, number>
|
|
348
|
+
z.set(z.string()) // Set<string>
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
</complex_types>
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
<validation>
|
|
356
|
+
|
|
357
|
+
## Optional & Nullable
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
z.string().optional() // string | undefined
|
|
361
|
+
z.string().nullable() // string | null
|
|
362
|
+
z.string().nullish() // string | null | undefined
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
## Default Values
|
|
366
|
+
|
|
367
|
+
```typescript
|
|
368
|
+
z.string().default("default")
|
|
369
|
+
z.number().default(0)
|
|
370
|
+
z.boolean().default(false)
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Enums
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// ✅ Native enum
|
|
377
|
+
enum Fruits {
|
|
378
|
+
Apple,
|
|
379
|
+
Banana,
|
|
380
|
+
}
|
|
381
|
+
z.nativeEnum(Fruits)
|
|
382
|
+
|
|
383
|
+
// ✅ Zod enum
|
|
384
|
+
const fruits = z.enum(['apple', 'banana', 'orange'])
|
|
385
|
+
type Fruit = z.infer<typeof fruits> // 'apple' | 'banana' | 'orange'
|
|
386
|
+
|
|
387
|
+
// ✅ 값 추출
|
|
388
|
+
fruits.enum.apple // 'apple'
|
|
389
|
+
fruits.options // ['apple', 'banana', 'orange']
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
## Literals
|
|
393
|
+
|
|
394
|
+
```typescript
|
|
395
|
+
z.literal('hello')
|
|
396
|
+
z.literal(42)
|
|
397
|
+
z.literal(true)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
</validation>
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
<transforms>
|
|
405
|
+
|
|
406
|
+
## Transform
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
const schema = z.string()
|
|
410
|
+
.transform((val) => val.length)
|
|
411
|
+
.pipe(z.number().positive())
|
|
412
|
+
|
|
413
|
+
schema.parse("hello") // → 5
|
|
414
|
+
schema.parse("") // Error: number must be positive
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
## Preprocess
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
const schema = z.preprocess(
|
|
421
|
+
(val) => (val === "" ? undefined : val),
|
|
422
|
+
z.string().optional()
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
schema.parse("") // → undefined
|
|
426
|
+
schema.parse("hello") // → "hello"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
## Coerce
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
z.coerce.string() // String(val)
|
|
433
|
+
z.coerce.number() // Number(val)
|
|
434
|
+
z.coerce.boolean() // Boolean(val)
|
|
435
|
+
z.coerce.date() // new Date(val)
|
|
436
|
+
|
|
437
|
+
// ✅ 예시
|
|
438
|
+
z.coerce.number().parse("42") // → 42
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
</transforms>
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
<refinements>
|
|
446
|
+
|
|
447
|
+
## Refine
|
|
448
|
+
|
|
449
|
+
```typescript
|
|
450
|
+
const schema = z.string()
|
|
451
|
+
.refine((val) => val.length >= 5, {
|
|
452
|
+
error: "Must be at least 5 characters.",
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
// ✅ v4: 체이닝 가능
|
|
456
|
+
z.string()
|
|
457
|
+
.refine((val) => val.includes("@"))
|
|
458
|
+
.min(5)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## SuperRefine
|
|
462
|
+
|
|
463
|
+
```typescript
|
|
464
|
+
const schema = z.object({
|
|
465
|
+
password: z.string(),
|
|
466
|
+
confirmPassword: z.string(),
|
|
467
|
+
}).superRefine((data, ctx) => {
|
|
468
|
+
if (data.password !== data.confirmPassword) {
|
|
469
|
+
ctx.addIssue({
|
|
470
|
+
code: z.ZodIssueCode.custom,
|
|
471
|
+
path: ['confirmPassword'],
|
|
472
|
+
message: "Passwords don't match.",
|
|
473
|
+
})
|
|
474
|
+
}
|
|
475
|
+
})
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
</refinements>
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
<error_handling>
|
|
483
|
+
|
|
484
|
+
## Error Handling
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
try {
|
|
488
|
+
schema.parse(data)
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (error instanceof z.ZodError) {
|
|
491
|
+
console.log(error.issues)
|
|
492
|
+
// [{
|
|
493
|
+
// code: 'invalid_type',
|
|
494
|
+
// expected: 'string',
|
|
495
|
+
// received: 'number',
|
|
496
|
+
// path: ['name'],
|
|
497
|
+
// message: 'Expected string, received number'
|
|
498
|
+
// }]
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ✅ Safe parse
|
|
503
|
+
const result = schema.safeParse(data)
|
|
504
|
+
if (!result.success) {
|
|
505
|
+
console.log(result.error.format())
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
## Custom Errors
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
const schema = z.string().min(5, { error: "Too short." })
|
|
513
|
+
const schema = z.email({ error: "Invalid email." })
|
|
514
|
+
|
|
515
|
+
// ✅ v4: error 파라미터 통합
|
|
516
|
+
z.number().int({ error: "Must be integer." })
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
## Prettify Errors
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
import { z } from 'zod'
|
|
523
|
+
|
|
524
|
+
const error = schema.safeParse(data).error
|
|
525
|
+
const pretty = z.prettifyError(error)
|
|
526
|
+
console.log(pretty)
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
</error_handling>
|
|
530
|
+
|
|
531
|
+
---
|
|
532
|
+
|
|
533
|
+
<advanced>
|
|
534
|
+
|
|
535
|
+
## Template Literals
|
|
536
|
+
|
|
537
|
+
```typescript
|
|
538
|
+
const css = z.templateLiteral([
|
|
539
|
+
z.number(),
|
|
540
|
+
z.enum(["px", "em", "rem"]),
|
|
541
|
+
])
|
|
542
|
+
|
|
543
|
+
type CSS = z.infer<typeof css>
|
|
544
|
+
// → `${number}px` | `${number}em` | `${number}rem`
|
|
545
|
+
|
|
546
|
+
css.parse("16px") // ✅
|
|
547
|
+
css.parse("16") // ❌
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## String Bool
|
|
551
|
+
|
|
552
|
+
```typescript
|
|
553
|
+
const schema = z.stringbool()
|
|
554
|
+
|
|
555
|
+
schema.parse("true") // → true
|
|
556
|
+
schema.parse("1") // → true
|
|
557
|
+
schema.parse("yes") // → true
|
|
558
|
+
schema.parse("on") // → true
|
|
559
|
+
schema.parse("false") // → false
|
|
560
|
+
schema.parse("0") // → false
|
|
561
|
+
schema.parse("no") // → false
|
|
562
|
+
schema.parse("off") // → false
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## File Validation
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
const schema = z.file()
|
|
569
|
+
.min(1024) // 1KB 이상
|
|
570
|
+
.max(10 * 1024 * 1024) // 10MB 이하
|
|
571
|
+
.mime(['image/png', 'image/jpeg'])
|
|
572
|
+
|
|
573
|
+
// ✅ 사용
|
|
574
|
+
schema.parse(fileInput.files[0])
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
## JSON Schema
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
const schema = z.object({
|
|
581
|
+
name: z.string(),
|
|
582
|
+
age: z.number(),
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
const jsonSchema = z.toJSONSchema(schema)
|
|
586
|
+
// {
|
|
587
|
+
// type: 'object',
|
|
588
|
+
// properties: {
|
|
589
|
+
// name: { type: 'string' },
|
|
590
|
+
// age: { type: 'number' }
|
|
591
|
+
// },
|
|
592
|
+
// required: ['name', 'age']
|
|
593
|
+
// }
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
## Promise & Function Schemas
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
const promiseSchema = z.promise(z.string())
|
|
600
|
+
const fnSchema = z.function()
|
|
601
|
+
.args(z.string(), z.number())
|
|
602
|
+
.returns(z.boolean())
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
</advanced>
|
|
606
|
+
|
|
607
|
+
---
|
|
608
|
+
|
|
609
|
+
<patterns>
|
|
610
|
+
|
|
611
|
+
## TanStack Start Patterns
|
|
612
|
+
|
|
613
|
+
| 용도 | 패턴 |
|
|
614
|
+
|------|------|
|
|
615
|
+
| **Server Function** | `.inputValidator(schema)` |
|
|
616
|
+
| **Search Params** | `validateSearch: schema` |
|
|
617
|
+
| **Form 검증** | `schema.safeParse(formData)` |
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
// ✅ POST Server Function
|
|
621
|
+
const createPostSchema = z.object({
|
|
622
|
+
title: z.string().min(1).max(100),
|
|
623
|
+
content: z.string().min(1),
|
|
624
|
+
tags: z.array(z.string()).max(5),
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
export const createPost = createServerFn({ method: 'POST' })
|
|
628
|
+
.inputValidator(createPostSchema)
|
|
629
|
+
.handler(async ({ data }) => prisma.post.create({ data }))
|
|
630
|
+
|
|
631
|
+
// ✅ Search Params
|
|
632
|
+
export const Route = createFileRoute('/posts')({
|
|
633
|
+
validateSearch: z.object({
|
|
634
|
+
page: z.number().catch(1),
|
|
635
|
+
sort: z.enum(['newest', 'oldest']).catch('newest'),
|
|
636
|
+
}),
|
|
637
|
+
})
|
|
638
|
+
|
|
639
|
+
// ✅ Form 검증
|
|
640
|
+
const formSchema = z.object({
|
|
641
|
+
email: z.email(),
|
|
642
|
+
password: z.string().min(8),
|
|
643
|
+
})
|
|
644
|
+
|
|
645
|
+
const handleSubmit = (formData: FormData) => {
|
|
646
|
+
const result = formSchema.safeParse({
|
|
647
|
+
email: formData.get('email'),
|
|
648
|
+
password: formData.get('password'),
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
if (!result.success) {
|
|
652
|
+
return { errors: result.error.flatten() }
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// result.data is type-safe
|
|
656
|
+
}
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
## Do's & Don'ts
|
|
660
|
+
|
|
661
|
+
| ✅ Do | ❌ Don't |
|
|
662
|
+
|-------|----------|
|
|
663
|
+
| `z.email()` 사용 | `z.string().email()` (deprecated) |
|
|
664
|
+
| `{ error: "..." }` 파라미터 | `{ message: "..." }` (v3) |
|
|
665
|
+
| `z.unknown()` 사용 | `z.any()` 남용 |
|
|
666
|
+
| `.safeParse()` 사용자 입력 | `.parse()` 사용자 입력 (throw) |
|
|
667
|
+
| 타입 추론 활용 | 중복 타입 정의 |
|
|
668
|
+
|
|
669
|
+
</patterns>
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
<migration>
|
|
674
|
+
|
|
675
|
+
## v3 → v4 Migration Checklist
|
|
676
|
+
|
|
677
|
+
| 항목 | 작업 |
|
|
678
|
+
|------|------|
|
|
679
|
+
| **String formats** | `z.string().email()` → `z.email()` |
|
|
680
|
+
| **Error messages** | `{ message: "..." }` → `{ error: "..." }` |
|
|
681
|
+
| **Object types** | `.strict()` → `z.strictObject()` |
|
|
682
|
+
| **Refinements** | 체이닝 가능 확인 |
|
|
683
|
+
| **Bundle size** | `zod/mini` 고려 (85% 감소) |
|
|
684
|
+
|
|
685
|
+
```typescript
|
|
686
|
+
// ❌ v3
|
|
687
|
+
const schema = z.object({
|
|
688
|
+
email: z.string().email({ message: "Invalid email." }),
|
|
689
|
+
age: z.number().min(18, { message: "Must be 18+." }),
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
// ✅ v4
|
|
693
|
+
const schema = z.object({
|
|
694
|
+
email: z.email({ error: "Invalid email." }),
|
|
695
|
+
age: z.number().min(18, { error: "Must be 18+." }),
|
|
696
|
+
})
|
|
54
697
|
```
|
|
55
698
|
|
|
56
|
-
</
|
|
699
|
+
</migration>
|