Skip to content

feat: let an entity declare another entity as a field - #14

Merged
btravers merged 2 commits into
mainfrom
feat/nest-entities
Aug 7, 2026
Merged

feat: let an entity declare another entity as a field#14
btravers merged 2 commits into
mainfrom
feat/nest-entities

Conversation

@btravers

@btravers btravers commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The README's fourth pillar — "something that nests inside other entities"
did not work. OnlyNominal rejected Customer.instance, so an aggregate had
to be a plain z.object(...) and lost make, update, invariants,
immutability and _tag. What "nested" was a schema, not an entity.

class Order extends Entity("Order")({
  id: OrderId,
  customer: Customer.instance,
  watchers: z.array(Customer.instance),
}) {}

What works, all pinned by tests

  • nested entities are real instances, keeping their computed fields, _tag
    and equals
  • they nest inside array fields too
  • an invariant can span the outer entity and a nested one
  • a nested validation failure reports the full path — ["customer", "name"]
  • JSON.stringify walks the tree to plain data, and the result feeds back
    through make
  • updating a sibling field leaves the nested entity intact
  • the outer deep-freeze does not strip a nested entity's prototype methods

How an entity is recognised

Structurally, on the three members every entity instance has (toJSON,
equals, update) — not against BaseInstance itself. That interface is
generic in the entity's own shape and there is no argument matching every
entity: never is too narrow to match any (measured — it rejected the field
outright), and the field map has no way to name the specific one.

instance now carries _tag

Its type was BaseInstance & DeepReadonly<Output>, omitting the _tag the
runtime has always set. Nested pattern matching with P.tag(...) did not
typecheck. Fixed, and pinned in shape.test-d.ts.

The error message

Rejecting a genuinely unbranded field used to produce a tuple TypeScript
truncated to & [...], hiding the advice behind a wall of
ZodType<BaseInstance<…>>:

Type 'ZodString' is not assignable to type 'ZodString & DomainFieldMustBeBrandedOrAnEntity'
  Property '__domainFieldMustBeBrandedOrAnEntity' is missing in type 'ZodString'

The name is the message now, so it survives truncation. (Written as a type
rather than an interface for consistent-type-definitions; verified the
alias still prints by name rather than being expanded.)

Gate

format --check, lint, typecheck (three passes), test (103, 10 files),
knip, build — all green.

Copilot AI lite review requested due to automatic review settings August 6, 2026 23:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables entity aggregates to declare other entities’ .instance schemas as fields (including in arrays), so nested entities remain real instances with behaviour, computed fields, and _tag, while keeping validation paths and JSON round-tripping intact.

Changes:

  • Relax shape()’s type-level OnlyNominal gate to accept entity instances (identified structurally), allowing Customer.instance to be used as a field type.
  • Add _tag to the static instance / ~standard TypeScript surface to match runtime behaviour and support typed tag-based matching.
  • Add a dedicated nesting.spec.ts runtime test suite plus type-level assertions; update READMEs and publish a minor changeset.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
README.md Updates docs to describe aggregates as entities and show nested entity behaviour.
packages/entity/src/types.ts Extends instance / ~standard inferred types to include _tag.
packages/entity/src/shape.ts Updates OnlyNominal to treat entity instances as acceptable domain fields and improves rejection message readability.
packages/entity/src/shape.test-d.ts Adds type-level checks covering nested entity fields and _tag presence/read-only behaviour.
packages/entity/src/nesting.spec.ts Adds runtime tests pinning nesting, invariants across entities, issue paths, JSON round-trips, and freeze behaviour.
packages/entity/README.md Updates package docs to reflect instance as a valid field for aggregates.
.changeset/nest-entities.md Declares a minor release and summarizes the new nesting capability and error-message improvement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@btravers
btravers merged commit 10739be into main Aug 7, 2026
13 checks passed
@btravers
btravers deleted the feat/nest-entities branch August 7, 2026 00:06
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.

2 participants