--- id: wiki-2026-0508-parse-dont-validate title: Parse, Don't Validate category: 10_Wiki/Topics status: verified canonical_id: self aliases: [parse don't validate, type-driven design, smart constructor, refinement type] duplicate_of: none source_trust_level: A confidence_score: 0.95 verification_status: applied tags: [type-system, design, haskell, typescript, validation] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript/Haskell framework: Zod, Effect, Brand types --- # Parse, Don't Validate ## 매 한 줄 > **"매 unsafe input 매 한번 parse → 매 typed value 매 produce, 매 downstream 매 다시 검증 의 X"**. 매 2019 Alexis King (lexi-lambda) 의 Haskell post 의 origin. 매 핵심 idea: 매 validation 매 boolean 의 throw — 매 information 의 lose. 매 parsing 매 validated 형식 의 새 type 의 produce — 매 type system 의 매 invariant 의 carry. ## 매 핵심 ### 매 validate 의 problem ```ts // 매 anti-pattern function isNonEmpty(arr: T[]): boolean { return arr.length > 0; } function head(arr: T[]): T { if (!isNonEmpty(arr)) throw new Error("empty!"); return arr[0]; // 매 type system 매 still T | undefined } ``` - 매 `isNonEmpty` check 후 매 type 매 `T[]` (그대로) — 매 information lost. - 매 head 매 매번 다시 check or throw. - 매 caller 매 invariant 의 untracked. ### 매 parse 의 solution ```ts type NonEmpty = readonly [T, ...T[]]; function parseNonEmpty(arr: T[]): NonEmpty | null { return arr.length > 0 ? (arr as NonEmpty) : null; } function head(arr: NonEmpty): T { return arr[0]; // 매 항상 safe — type system 의 guarantee } ``` - 매 parse 결과 매 새 type — 매 invariant 가 매 type 에 baked in. - 매 downstream 매 trust — 매 re-check 의 X. ### 매 응용 1. API request body validation (Zod / Effect Schema). 2. ID type discrimination (UserId vs OrderId). 3. URL / Email parsing. 4. Smart constructor (private constructor + parse function). 5. Domain modeling (PositiveNumber, NonEmptyString). ## 💻 패턴 ### 1. Branded type (TypeScript) ```ts type Brand = T & { readonly __brand: B }; type Email = Brand; function parseEmail(s: string): Email | null { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s) ? (s as Email) : null; } function sendMail(to: Email, body: string) { /* 매 trust to */ } const raw = "user@example.com"; const email = parseEmail(raw); if (!email) throw new Error("bad email"); sendMail(email, "hi"); // 매 OK sendMail(raw, "hi"); // 매 type error ``` ### 2. Zod schema (parse-style) ```ts import { z } from 'zod'; const UserSchema = z.object({ id: z.string().uuid(), email: z.string().email(), age: z.number().int().min(0).max(150), }); type User = z.infer; // 매 fully-typed app.post('/users', (req, res) => { const result = UserSchema.safeParse(req.body); if (!result.success) return res.status(400).json(result.error); createUser(result.data); // 매 trust — User type 매 guaranteed }); ``` ### 3. Effect Schema (2026 의 매 modern) ```ts import { Schema as S } from "effect"; const PositiveInt = S.Int.pipe(S.positive(), S.brand("PositiveInt")); type PositiveInt = S.Schema.Type; const decode = S.decodeUnknownSync(PositiveInt); const x = decode(42); // 매 PositiveInt const y = decode(-1); // 매 throws ParseError ``` ### 4. Smart constructor (Haskell-style) ```ts class NonEmptyList { private constructor(public readonly items: readonly T[]) {} static parse(items: readonly T[]): NonEmptyList | null { return items.length > 0 ? new NonEmptyList(items) : null; } get head(): T { return this.items[0]; } // 매 always safe get tail(): readonly T[] { return this.items.slice(1); } } ``` ### 5. Discriminated ID type ```ts type UserId = Brand; type OrderId = Brand; function getUser(id: UserId): User { /*...*/ } function getOrder(id: OrderId): Order { /*...*/ } const uid = parseUserId(req.params.id); if (!uid) throw new Error(); getUser(uid); // 매 OK getOrder(uid); // 매 type error 매 prevent mix-up ``` ### 6. Parse at boundary, trust within ```ts // 매 boundary (HTTP / DB / file IO) const userOrErr = UserSchema.safeParse(rawJson); // 매 internal — User type 매 항상 valid function processUser(u: User) { // 매 u.email 매 valid email — 매 re-check 의 X // 매 u.age 매 0-150 매 — 매 re-check 의 X } ``` ### 7. Refinement chain ```ts const NonEmptyString = z.string().min(1).brand<'NonEmptyString'>(); const EmailString = NonEmptyString.refine( s => /^[^@]+@[^@]+$/.test(s) ).brand<'Email'>(); type Email = z.infer; ``` ## 매 결정 기준 | 상황 | Apply parse-don't-validate? | |---|---| | Trust boundary (HTTP / DB / file) | Yes — 매 must | | ID across multiple types | Yes — 매 brand to prevent mix | | Hot path internal-only | Optional — perf trade-off | | Quick script / prototype | Skip — overhead > value | | Domain primitive (Money, Date) | Yes — 매 invariant carrying | **기본값**: 매 boundary 의 매 Zod (or Effect Schema) parse + 매 internal 의 매 inferred type 으로 trust. ## 🔗 Graph - 부모: [[Type-Driven Design]] - 변형: [[Refinement Type]] · [[Smart Constructor]] - 응용: [[Zod]] · [[Effect Schema]] · [[Branded Type]] - Adjacent: [[Validation]] · [[Type Narrowing]] ## 🤖 LLM 활용 **언제**: 매 API server / public library 의 매 input validation. 매 ID mix-up bug 매 routine 한 codebase. 매 domain rule 매 type-encode 가능 한 경우. **언제 X**: 매 internal-only quick script. 매 highly dynamic JSON 의 schema 가 unknown. 매 perf-critical hot loop (parse overhead). ## ❌ 안티패턴 - **Validate 후 raw type 의 pass**: 매 invariant 매 lose. 매 항상 새 type 의 return. - **Parse 매 boundary X 의 매 매 layer 의 repeat**: 매 perf 손실 + 매 동일 logic 의 duplicate. - **Brand 의 매 runtime check 의 X**: 매 cast 매 type-only — 매 parse function 매 항상 runtime check 포함. - **Optional 의 abuse**: 매 `email?: string` — 매 invariant 매 unclear. 매 명확한 `Email | null`. - **Throw on parse fail (preference)**: 매 Result type / safeParse 의 매 prefer — 매 caller flow 매 explicit. ## 🧪 검증 / 중복 - Verified (Alexis King "Parse, Don't Validate" 2019, Zod 4 / Effect 3.x docs 2026-05). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — Branded types + Zod/Effect Schema 의 modern parse pattern |