Functional Domain Modeling in TypeScript
Robust server-side TypeScript design with better-result (Result, TaggedError) and valibot (schemas, branded types).
Invariants
Apply these throughout:
- Model each entity state as its own
Readonly<>type; discriminate withkindproject-wide. - Define domain types with
type, neverinterface; write functions in type definitions with function property notation (save: (t: Task) => Promise<void>). - Express state transitions as pure functions whose argument types constrain valid source states and whose return types name the target state. Invalid transitions must be compile errors.
- Errors are values: domain code returns
Result<T, E>and never throws. Define each known failure as aTaggedErrorclass; reservepanicfor violated invariants and programmer mistakes. - Compose fallible steps with
Result.genby default; use a lone combinator only for a single transform. Avoid long combinator chains. - Validate every external input (API request, DB row, env var, file, queue message) with a valibot schema at the boundary; trust types inside the domain — do not re-validate. When a schema already defines a boundary representation, derive its type with
v.InferOutput/v.InferInputinstead of restating the shape. asis banned exceptas constandas const satisfies T. When a value's type is unknown, parse it through a schema. Brand IDs withv.brandso no cast is ever needed.
Topics
Read only the reference file(s) relevant to the current task.
| Task involves | Read |
|---|---|
| Entity types, branded IDs, companion objects, file layout | references/domain-modeling.md |
| State transitions, domain events, repository interfaces | references/state-modeling.md |
| TaggedError design, Result composition, HTTP error mapping | references/error-handling.md |
| Parsing external input, schema→Result factory, PII protection | references/boundary-defense.md |
| Collection operations, test fixtures | references/style-and-testing.md |
| Setting up lint enforcement (oxlint) | references/linting.md |