Skip to content
This repository was archived by the owner on Oct 7, 2026. It is now read-only.

fix: keep zod's brand nameable through DeepReadonly - #156

Merged
btravers merged 1 commit into
mainfrom
fix/152-factory-nested-emit
Oct 6, 2026
Merged

btravers merged 1 commit into
mainfrom
fix/152-factory-nested-emit

Conversation

@btravers

@btravers btravers commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Root cause: DeepReadonly mapped a branded object (z.object(…).brand("Money")) as a whole. The brand became { readonly [$brand]: … }, a computed key over zod's unique symbol, and a downstream declaration emit cannot name that (TS4023). It only surfaced where the emitter has to print DeepReadonly written out in full, such as an exported X.factory(...) whose input contains a nested entity's instance type. Branded object fields break declaration emit with TS4020 (OnlyNominal accepts them) #32 hit the same root through a different path.
  • Fix: a branded non-primitive is now peeled, mapped and re-branded: DeepReadonly<Omit<T, typeof $brand>> & z.core.$brand<B>, which prints as z.$brand<"Money">. Branded primitives still short-circuit through Immutable. The Branded object fields break declaration emit with TS4020 (OnlyNominal accepts them) #32 guard (invoice.total.amount = 1 is a compile error) still holds, since its @ts-expect-error is still used.
  • Regression guard: examples/billing-domain now exports createOrder = Order.factory(...) directly. It failed before the fix with TS4023 on 7.0.2 and 5.9.3, and it is checked by both declaration-emit passes plus the 5.9.3 check of the emitted output. This removes docs(aggregate): draw ownership, reference and snapshot boundaries #151's private-factory workaround.
  • Emit size: the fix alone takes the fixture's emitted .d.ts from 40,863 to 40,708 bytes (−155 B). The new createOrder export adds 2.4 KB of its own. There are still zero GeneratedKeys< / ImmutableKeys< in the output.
  • Patch changeset.

Closes #152

Test plan

  • Red first: exporting Order.factory failed tsc -p tsconfig.emit.json with TS4023
  • Full gate: format, lint, typecheck (incl. .test-d.ts and both emit passes), test (forced), knip, build
  • CI green

🤖 Generated with Claude Code

DeepReadonly mapped a branded object whole, turning its brand into a
computed `[$brand]` key over zod's unique symbol. Wherever a consumer's
declaration emit had to print that type expanded, such as an exported
factory of an entity with a nested-entity field, emit failed with
TS4023 on both 7.0.2 and 5.9.3.

Peel the brand, map the rest, and re-intersect `z.core.$brand<B>`, which
prints by name. The billing fixture now exports `Order.factory` as the
regression guard; the fix alone shrinks its emitted declarations by
155 bytes.

Closes #152

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 6, 2026 08:50
@coderabbitai

coderabbitai Bot commented Oct 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 5 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: btravstack/entity/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 019a5dfb-14a0-4c76-b505-e25b2b61dac9
📥 Commits

Reviewing files that changed from the base of the PR and between e0ce701 and 8d928cd.

📒 Files selected for processing (3)
  • .changeset/branded-object-declaration-emit.md
  • examples/billing-domain/src/order.ts
  • packages/entity/src/types.ts
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@btravers
btravers merged commit fa17717 into main Oct 6, 2026
14 of 15 checks passed
@btravers
btravers deleted the fix/152-factory-nested-emit branch October 6, 2026 08:57
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exporting factory() of an entity with a nested-entity field fails declaration emit (TS4023 $brand)

2 participants