--- name: value-object description: Add a validated input type (postcode, email, hours, id number, …) as a branded value object with a parser — "parse, don't validate". Use whenever a form field or API value has format rules. --- # Value object (parse, don't validate) Raw input becomes a branded type only via a parser returning `Result`. Once you hold the type, never re-check it. Never model validity as a boolean flag next to a string. ## Skeleton `/domain/value-objects/.ts` (pure TS, no Angular): ```ts import { Brand, Result, ok, err } from '@shared/kernel/fp'; export type Postcode = Brand; export function parsePostcode(raw: string): Result { const t = raw.trim().toUpperCase(); if (!/^[1-9]\d{3}\s?[A-Z]{2}$/.test(t)) { return err($localize`:@@validation.postcode:Voer een geldige postcode in, bijv. 1234 AB.`); } // The parser also normalises — callers always hold the canonical form. return ok(t.replace(/^(\d{4})\s?([A-Z]{2})$/, '$1 $2') as Postcode); } ``` Rules: - The `as Brand` cast appears **only** inside the parser — the type is mintable nowhere else. - Error message is user-facing → `$localize` with a stable `@@validation.` id. - Trim/normalise before testing; return the cleaned value. - This is **format** feedback only. The server re-validates as authority (ADR-0001) — never encode business rules (existence, eligibility) here. ## Spec (required) Co-located `.spec.ts`: happy path, normalisation, each rejection case. Call the parser directly — no TestBed. ## Wiring into a form The machine's `validate(draft)` calls the parsers and collects errors into `StepErrors`; the `Valid` type holds the branded values (see **form-machine** skill). ## Worked examples `src/app/registratie/domain/value-objects/` — `postcode.ts`, `email.ts`, `uren.ts`, `big-nummer.ts`, each with a co-located spec. ## Verify ```bash npm test && npm run lint ```