Skip to content

Type hygiene for the domain-object factories, and DomainObjectDTO's name mismatch - #30

Merged
lhellemons merged 3 commits into
mainfrom
design/factory-hygiene
Aug 4, 2026
Merged

Type hygiene for the domain-object factories, and DomainObjectDTO's name mismatch#30
lhellemons merged 3 commits into
mainfrom
design/factory-hygiene

Conversation

@lhellemons

Copy link
Copy Markdown
Owner

Summary

Three focused fixes to src/value-object and src/domain, closing gaps flagged in issue #10 and a pre-1.0 type-hygiene pass.

1. Error type parameter for the factories. 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 whole pitch) is 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, to translate a thrown value into that subclass. E needs to be named explicitly alongside T when using it — Success's invariant E phantom (docs/adr/0001's 2026-08-04 amendment) keeps TypeScript from reliably inferring E from errorHandler'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 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, not assumed: a standalone compiler probe showed 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.

3. DomainObjectDTO name/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 on DTOSource<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 check passes after every commit.

Test plan

  • pnpm check (format, lint, typecheck, test:coverage) passes on the final branch state
  • New .test-d.ts files pin the E inference behavior (including the invariant-phantom sharp edge) and the T, P order improvement
  • New runtime tests exercise errorHandler and a factory with a narrowed Failure subclass
  • DTOSource round-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

lhellemons and others added 3 commits August 4, 2026 10:00
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>
@lhellemons
lhellemons force-pushed the design/factory-hygiene branch from 296b8e2 to ecb05ac Compare August 4, 2026 08:04
@lhellemons

Copy link
Copy Markdown
Owner Author

Rebased onto main (which since merged #28, #29, and #31) — conflicts resolved, all three commits preserved, pnpm check and pnpm assert:exports both green.

Conflict resolution

CHANGELOG.md / README.md / CONTEXT.md — both-added conflicts. Kept both sides' entries:

The real work: tryCatch's overload split

#31 split result/tryCatch into two overloads — tryCatch(fn) fixes E = Error, tryCatch(fn, handler) stays generic in E — specifically so a handler-less call can never mis-claim a narrower E than the default handler actually produces. This PR's definePrimitiveValueObject used to forward its optional errorHandler straight through to tryCatch(construct, errorHandler); with the split, that call no longer type-checks when errorHandler is undefined (no single overload accepts an optionally-undefined handler).

Fixed by mirroring the split onto definePrimitiveValueObject itself: it's now two public overloads plus one optional-handler implementation signature —

  • no errorHandlerPrimitiveValueObject<T, P, Error>, routing through tryCatch's handler-less overload
  • errorHandler: Mapper<unknown, Result<T, E>> → generic in E, routing through tryCatch's generic overload

for the same reason tryCatch itself splits: naming E without an errorHandler would otherwise let a thrown value that isn't actually E sail through uncaught. Docblocks and the CHANGELOG entry were reworded to describe the split rather than the old "forwarded to tryCatch verbatim" framing. The test-d.ts inference claims (defaults to Error with no handler; narrows to the handler's concrete type when E is named alongside T) still hold as written — verified against the new overloads, not just carried over.

failure<T, E>failure<E, T> reorder

#31 also reordered result/failure's type parameters from <T, E> to <E, T>. src/domain/index.test.ts's example factories used the old order (failure<User>(...), failure<User, EmptyName>(...)); main had already fixed the first one for its own pre-existing UserFactory test. The second — the StrictUserFactory example this PR added — needed the same swap, now failure<EmptyName, User>(...).

Commit structure

All three original commits rebased cleanly onto the new base with no need to squash:

  1. Give factories a concrete Error type parameter (now includes the tryCatch-overload reconciliation above)
  2. Align definePrimitiveValueObject's type parameter order with PrimitiveValueObject
  3. Rename DomainObjectDTO to DTOSource

🤖 Generated with Claude Code

@lhellemons
lhellemons merged commit 05b519b into main Aug 4, 2026
1 check passed
lhellemons added a commit that referenced this pull request Aug 4, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant