Type hygiene for the domain-object factories, and DomainObjectDTO's name mismatch - #30
Conversation
PrimitiveValueObject.from and DomainObjectFactory.from were both pinned to Result<T, Error>, so a factory could never declare the concrete Failure subclass its .from reports - even though CONTEXT.md's Failure entry and this library's own pitch are that a Failure keeps its concrete subclass so a caller can branch on it. Both gain E extends Error = Error. definePrimitiveValueObject gains an optional errorHandler, forwarded to tryCatch verbatim, so a caller can translate a thrown value into that subclass; E must be named explicitly alongside T when doing so, since Success's invariant E phantom (docs/adr/0001, 2026-08-04 amendment) keeps inference from picking it up from errorHandler's return type alone - confirmed by trying inference-only first and watching it default back to Error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eValueObject definePrimitiveValueObject<P, T> named its type parameters in the opposite order from the type it builds, PrimitiveValueObject<T, P>. Reordered to T, P, E, and gave P a default of string at the function itself (previously only the type had one), so the common case is one explicit type argument: definePrimitiveValueObject<Email>(...). Measured rather than assumed: the old order let a single explicit type argument compile silently as P instead of T, leaving T as unknown - a footgun the new order and default close off, confirmed with a standalone compiler probe before touching the real signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DomainObjectDTO = { readonly dto: TDTO } was a domain object carrying
its own DTO, but CONTEXT.md defines DTO as the plain, untrusted data
itself - the library's own glossary and its type disagreed. Renamed to
DTOSource, which names what the type is: the other direction of a
Factory's seam, recovering the DTO a domain object's current values
would round-trip back through the Factory that built it.
Same shape, same signature - only the name changed. CONTEXT.md gains a
matching "DTO Source" entry next to Factory, and PR #25's round-trip
test (src/domain/index.test.ts) is updated to the new name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
296b8e2 to
ecb05ac
Compare
|
Rebased onto Conflict resolutionCHANGELOG.md / README.md / CONTEXT.md — both-added conflicts. Kept both sides' entries:
The real work:
|
The API-coherence milestone (#28-#31, #30) edited docs module-by-module; this harmonizes them. Docblocks stop narrating their own history ("under the previous <T, E> order...", "before this existed...") and their authors' experiments ("confirmed by trying it") — that record lives in the CHANGELOG and ADRs — while keeping the caller-facing rationale that is this library's signature. Doubled rationale in fn/curry is stated once. Drift fixes: the README module table was missing internByKey and TrackedState, and the Status section credited the unreleased result/mapError to v0.2.x; it now separates the released combinator set from the unreleased batch explicitly. Docs only: no signature, type, or behaviour changes, and no type-level diagnostic strings touched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Three focused fixes to
src/value-objectandsrc/domain, closing gaps flagged in issue #10 and a pre-1.0 type-hygiene pass.1. Error type parameter for the factories.
PrimitiveValueObject.fromandDomainObjectFactory.fromwere both pinned toResult<T, Error>, so a factory could never declare the concrete Failure subclass its.fromreports — even though CONTEXT.md's Failure entry (and this library's whole pitch) is that a Failure keeps its concrete subclass so a caller can branch on it. Both gainE extends Error = Error.definePrimitiveValueObjectgains an optionalerrorHandler, forwarded totryCatchverbatim, to translate a thrown value into that subclass.Eneeds to be named explicitly alongsideTwhen using it —Success's invariantEphantom (docs/adr/0001's 2026-08-04 amendment) keeps TypeScript from reliably inferringEfromerrorHandler's return type alone, confirmed with a standalone compiler probe before touching the real signature, not assumed.2. Type-parameter order alignment.
definePrimitiveValueObject<P, T>named its type parameters in the reverse order from the type it builds,PrimitiveValueObject<T, P>. Reordered toT, P, E, and gavePa default ofstringat the function itself (previously only the type had one), so the common case is one explicit type argument:definePrimitiveValueObject<Email>(...). Measured, not assumed: a standalone compiler probe showed the old order let a single explicit type argument compile silently asPinstead ofT, leavingTasunknown— a footgun the new order and default close off.3.
DomainObjectDTOname/meaning mismatch. The type was{ readonly dto: TDTO }— a domain object carrying its own DTO — but CONTEXT.md defines DTO as the plain, untrusted data itself. Asked the user how to resolve it; the answer was to rename rather than redefine the glossary or drop the type. After a second round on the specific name, landed onDTOSource<TDTO>— the other direction of a Factory's seam, recovering the DTO a domain object's current values would round-trip back through the Factory that built it. Same shape, same signature, only the name changed. CONTEXT.md gains a matching "DTO Source" entry next to Factory, and PR #25's round-trip test is updated to the new name.Each item is its own commit.
pnpm checkpasses after every commit.Test plan
pnpm check(format, lint, typecheck, test:coverage) passes on the final branch state.test-d.tsfiles pin theEinference behavior (including the invariant-phantom sharp edge) and theT, Porder improvementerrorHandlerand a factory with a narrowed Failure subclassDTOSourceround-trip and interning tests (from PR Connect InternRegistry to CompoundValueObject's key; add DTO round-trip test #25) still pass under the new name🤖 Generated with Claude Code